$ bundle exec rails new techlog-app --css tailwind
create
create README.md
create Rakefile
...
Using bootsnap 1.12.0
Bundle complete! 16 Gemfile dependencies, 75 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
この時、ご利用の環境により cannot load such file -- bootsnap/setup (LoadError) という 以下のようなエラーメッセージが出力される場合があります。
run bundle binstubs bundler
rails importmap:install
/Users/YourName/workspace/techlog-app/config/boot.rb:4:in `require': cannot load such file -- bootsnap/setup (LoadError)
$ bundle install
Fetching gem metadata from https://rubygems.org/..........
Fetching rake 13.0.6
Installing rake 13.0.6
...
Fetching rails 7.0.3
Installing rails 7.0.3
Bundle complete! 16 Gemfile dependencies, 75 gems now installed.
Bundled gems are installed into `./vendor/bundle`
Post-install message from rubyzip:
RubyZip 3.0 is coming!
**********************
The public API of some Rubyzip classes has been modernized to use named
parameters for optional arguments. Please check your usage of the
following classes:
* `Zip::File`
* `Zip::Entry`
* `Zip::InputStream`
* `Zip::OutputStream`
Please ensure that your Gemfiles and .gemspecs are suitably restrictive
to avoid an unexpected breakage when 3.0 is released (e.g. ~> 2.3.0).
See https://github.com/rubyzip/rubyzip for details. The Changelog also
lists other enhancements and bugfixes that have been implemented since
version 2.3.0.
$ bundle exec rails tailwindcss:install
Add Tailwindcss include tags and container element in application layout
insert app/views/layouts/application.html.erb
insert app/views/layouts/application.html.erb
insert app/views/layouts/application.html.erb
Build into app/assets/builds
create app/assets/builds
create app/assets/builds/.keep
append app/assets/config/manifest.js
append .gitignore
Add default config/tailwindcss.config.js
create config/tailwind.config.js
Add default app/assets/stylesheets/application.tailwind.css
create app/assets/stylesheets/application.tailwind.css
Add default Procfile.dev
create Procfile.dev
Ensure foreman is installed
run gem install foreman from "."
Fetching foreman-0.87.2.gem
Successfully installed foreman-0.87.2
Parsing documentation for foreman-0.87.2
Installing ri documentation for foreman-0.87.2
Done installing documentation for foreman after 0 seconds
1 gem installed
Add bin/dev to start foreman
create bin/dev
Compile initial Tailwind build
run rails tailwindcss:build from "."
...
Done in 176ms.
こちらも、上記のように各ファイルが作成されており、特にエラーが出ていなければ OK です。
動作確認 (rails s)
ここまで準備ができたら、最後に Rails サーバを起動して最低限の動作確認をしておきましょう。
Rails サーバを起動するには bundle exec rails s コマンドを実行します。
$ bundle exec rails s
=> Booting Puma
=> Rails 7.0.3 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.0.4-p208) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 64014
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitattributes
.gitignore
.ruby-version
Gemfile
Gemfile.lock
...
nothing added to commit but untracked files present (use "git add" to track)
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitattributes
new file: .gitignore
new file: .ruby-version
new file: Gemfile
new file: Gemfile.lock
...
$ git push -u origin main
Enter passphrase for key '/Users/YourName/.ssh/id_rsa':
...
- [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Saigen
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
end
end
まずは、見通しをよくするために一旦コメントアウトの部分は削除してしまいます。
config/application.rb (コメントアウト部分削除後)
require_relative "boot"
require "rails/all"
Bundler.require(*Rails.groups)
module TechLogApp
class Application < Rails::Application
config.load_defaults 7.0
end
end
その後、class Application の中に以下の 4 行を追記します。
class Application < Rails::Application
config.load_defaults 7.0
config.generators do |g| # ここから追記
g.assets false # CSS, JavaScriptファイルを自動生成しない
g.helper false # helperファイルを自動生成しない
end # ここまで追記
end
$ bundle exec spring binstub rspec
* bin/rspec: generated with Spring
実行すると、bin/ ディレクトリ内に rspec という実行用ファイルが作成されているはずです。
$ ls bin/
bundle importmap rake setup
dev rails rspec spring
最後に、テスト環境では Spring がキャッシュをしないよう、アプリケーション側で reload を有効にしてあげます。 config/environments/test.rbファイルを開き、config.cache_classesと書かれた 1 行を編集します。
config/environments/test.rb
config.cache_classes = false # trueからfalseに変更
これで Spring を使って RSpec を高速で起動する準備ができました。 実際に使ってみましょう。
Spring の恩恵を受けるためには bundle exec rspecではなく bin/rspecコマンドでテストを実行してあげます。
$ bin/rspec
DEBUGGER: Attaching after process 37366 fork to child process 37415
Running via Spring preloader in process 37415
No examples found.
Finished in 0.00047 seconds (files took 0.10171 seconds to load)
0 examples, 0 failures
$ bundle exec rubocop
Inspecting 12 files
C.........CC
Offenses:
Gemfile:58:3: C: [Correctable] Bundler/OrderedGems: Gems should be sorted in an alphabetical order within their section of the Gemfile. Gem factory_bot_rails should appear before rspec-rails.
gem 'factory_bot_rails' # 追加
^^^^^^^^^^^^^^^^^^^^^^^
spec/rails_helper.rb:6:7: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
abort("The Rails environment is running in production mode!") if Rails.env.production?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
spec/spec_helper.rb:49:1: C: [Correctable] Style/BlockComments: Do not use block comments.
=begin ...
^^^^^^
3 つの指摘が出ました。 今回は丁寧に一つずつ見ておきます。
・Bundler/OrderedGems: Gems should be sorted in an alphabetical order… Gemfile に gem 名を書く時はアルファベット順に書こうね、という指摘です。 直さなくても特に支障はないものの、現時点での順番に意味があるわけではないので直しておきましょう。 Correctableと書いてあるので、-aオプションを付ければ自動修正可能な指摘です。・Style/StringLiterals: Prefer single-quoted strings… RSpec 設定ファイル作成時、自動で生成されたファイルで “”を使った文字囲いがありました。 これもただ ” に直すだけなので、自動修正で OK です。・Style/BlockComments: Do not use block comments. これも自動生成されたファイルに関する指摘です。 =begin という書き方がコメントアウトをしていますが、パッと見でコメントであると分かりにくいため指摘が入っています。 コメントなので、直しても動作への影響はないため自動修正してしまいましょう。
以上 3 点、すべて自動修正で問題ないことを確認できたので -a オプションをつけて自動修正します。
$ bundle exec rubocop -a
Inspecting 12 files
C.........CC
Offenses:
Gemfile:58:3: C: [Corrected] Bundler/OrderedGems: Gems should be sorted in an alphabetical order within their section of the Gemfile. Gem factory_bot_rails should appear before rspec-rails.
gem 'factory_bot_rails' # 追加
^^^^^^^^^^^^^^^^^^^^^^^
spec/rails_helper.rb:6:7: C: [Corrected] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
abort("The Rails environment is running in production mode!") if Rails.env.production?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
spec/spec_helper.rb:49:1: C: [Corrected] Style/BlockComments: Do not use block comments.
=begin ...
^^^^^^
spec/spec_helper.rb:91:1: C: [Corrected] Layout/CommentIndentation: Incorrect indentation detected (column 0 instead of 2).
# Kernel.srand config.seed
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 files inspected, 4 offenses detected, 4 offenses corrected
$ bundle exec rails s
=> Booting Puma
=> Rails 7.0.3 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.0.4-p208) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 40714
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
require 'rails_helper'
RSpec.describe 'Home', type: :request do
describe 'GET /' do
it 'HTTP ステータス 200 を返す' do
get '/'
expect(response).to have_http_status(200)
end
end
end
require 'rails_helper' # rails_helper で定義した設定を読み込み
RSpec.describe 'Home', type: :request do # テスト対象モジュールとテストの種類
describe 'GET /' do # テストする機能に関する説明
it 'HTTP ステータス 200 を返す' do # どういう結果を期待しているか
get '/' # 確認したい操作: / というパスに GET でリクエストを投げる
expect(response).to have_http_status(200) # 期待する確認結果
end
end
end
$ bin/rspec
DEBUGGER[spring app | techlog-app | started 33 mins ago | test mode#42184]: Attaching after process 40675 fork to child process 42184
Running via Spring preloader in process 42184
Home
GET /
HTTP ステータス 200 を返す
Finished in 0.12033 seconds (files took 0.33231 seconds to load)
1 example, 0 failures
require 'rails_helper'
RSpec.describe "Homes", type: :system do
before do
driven_by(:rack_test)
end
pending "add some scenarios (or delete) #{__FILE__}"
end
$ bin/rspec spec/system/home_spec.rb
...
Pending: (Failures listed here are expected and do not affect your suite's status)
1) Homes add some scenarios (or delete) .../spec/system/home_spec.rb
# Not yet implemented
# ./spec/system/home_spec.rb:8
Finished in 0.00121 seconds (files took 0.36953 seconds to load)
1 example, 0 failures, 1 pending
このように System Spec で pendingと書いておくと、そのテストはどんな内容を書いていても実行されません。
require 'rails_helper'
RSpec.describe 'Home', type: :system do
before do
driven_by(:rack_test)
end
describe 'トップページの検証' do
it 'Home#top という文字列が表示される' do
visit '/'
expect(page).to have_content('Home#top')
end
end
end
テスト実行
それでは System Spec を実行してみましょう。 編集したテストのみを実行するには、いつもの bin/rspecに続いてファイル名を渡してあげます。
$ bin/rspec spec/system/home_spec.rb
...
Home
トップページの検証
DEBUGGER[rspec#44711]: Attaching after process 44690 fork to child process 44711
Home#top という文字列が表示される
Finished in 6.03 seconds (files took 0.33855 seconds to load)
1 example, 0 failures
...
RSpec.describe 'Home', type: :system do
before do # ここから追記
driven_by :selenium_chrome_headless
end # ここまで追記
この状態で再度 System Spec を走らせます。
$ bin/rspec spec/system/home_spec.rb
...
Home
/home/top 画面の検証
DEBUGGER[rspec#47924]: Attaching after process 47918 fork to child process 47924
Home#top という文字列が表示される
Finished in 1.48 seconds (files took 0.34879 seconds to load)
1 example, 0 failures
Gemfile を開き、以下の 1 行を追加します。 (devise は本番環境でも使用するため、group は development でも test でも無いところに記載してください。)
Gemfile
gem 'devise' # 追記
Gemfile に追記が完了したら、いつも通り bundle install を実行します。
$ bundle install
Fetching devise 4.8.1
Installing devise 4.8.1
Bundle complete! 24 Gemfile dependencies, 102 gems now installed.
Bundled gems are installed into `./.bundle`
初期設定
devise の初期設定に必要なファイルは、コマンドで作成できます。 devise をインストールすると rails g devise:xxx というコマンドがいくつか使えるようになるのですが、 その中の一つで rails g devise:install コマンドを最初は使います。
$ bundle exec rails g devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Depending on your application's configuration some manual setup may be required:
前回 rails g devise User を実行した際、テストに関連する以下のファイルが作成されていました。
spec/factories/users.rb
spec/models/user_spec.rb
今回はこれらを修正し、最後に bin/rspec 実行時に全てのテストが通ることを目標とします。
Factory ファイルの修正
spec/factories/users.rb は、以前導入した FactoryBot を用い、テストで User を簡単に作成するためのファイルです。
こちらを以下のように修正してください。
spec/factories/users.rb
FactoryBot.define do
factory :user do
nickname { 'テスト太郎' }
sequence :email do |n|
"test#{n}@example.com"
end
password { '111111' }
password_confirmation { '111111' }
end
end
ユニークキーである email の一部だけが異なる、複数の User を作成する Factory となっています。
User 生成&取得のテスト
次に User モデルの Spec であるspec/models/user_spec.rbを修正します。
まずは基本的なところとして、FactoryBot で生成した User を User.first で取得し、 取得した User が想定通りの属性(nickname, email) を持っていることを確認するテストを用意しましょう。
spec/models/user_spec.rb
require 'rails_helper'
describe User do
let(:nickname) { 'テスト太郎' }
let(:email) { 'test@example.com' }
describe '.first' do
before do
create(:user, nickname: nickname, email: email)
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
end
end
これでテストを実行してみます。
テストを実行する際、比較的時間がかかる System Spec まで一緒に実行すると開発スピードが落ちるため、User モデルの Spec だけを実行します。 特定のテストだけを実行したい場合は、bin/rspec の後に半角スペースを挟み、さらに実行したいテストのファイル名を渡します。
1 つだけですが、テストが通っていることを確認してください。
$ bin/rspec spec/models/user_spec.rb
...
User
.first
事前に作成した通りのUserを返す
Finished in 0.06006 seconds (files took 0.5011 seconds to load)
1 example, 0 failures
User.nickname のバリデーション(文字数)
ここでは User の nickname 属性に文字数制限のバリデーションを設定していきます。 その際、バリデーションのテストを作成し、それから実際にバリデーションを設定します。
※このように要件を満たすためのテストを事前に作成してから実装をすることをテスト駆動開発といいます。
バリデーションのテストを追加
今回は、ニックネームは 20 文字までという制限をかけていきます。
そこで、先にこのバリデーションに関するテストを作成しておき、それから実際に User モデルにバリデーションを設定します。 先にテストを書いておくことで仕様が明確になり、確実にバリデーションを実装することができます。
require 'rails_helper'
describe User do
let(:nickname) { 'テスト太郎' }
let(:email) { 'test@example.com' }
let(:password) { '12345678' }
let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) }
describe '.first' do
before do
create(:user, nickname: nickname, email: email)
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
end
describe 'validation' do
describe 'nickname属性' do
describe '文字数制限の検証' do
context 'nicknameが20文字以下の場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字
it 'User オブジェクトは有効である' do
expect(user.valid?).to be(true)
end
end
context 'nicknameが20文字を超える場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字
it 'User オブジェクトは無効である' do
user.valid?
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include('is too long (maximum is 20 characters)')
end
end
end
end
end
end
user という変数に作成した User オブジェクトを格納しています。
有効な user については user.valid? が true となることを確認しています。
一方、無効な user についてはuser.valid?実行時にuser.errros にエラー内容が入ることを利用したテストとなっています。
まだバリデーションを設定はしていないので、テストは失敗することを確認してください。
$ bin/rspec spec/models/user_spec.rb
...
Finished in 0.11684 seconds (files took 0.50221 seconds to load)
3 examples, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:35 # User validation nickname文字数制限の検証 nicknameが20文字を超える場合 User オブジェクトは無効である
$ bin/rspec spec/models/user_spec.rb
...
User
.first
事前に作成した通りのUserを返す
validation
nickname属性
文字数制限の検証
nicknameが20文字以下の場合
User オブジェクトは有効である
nicknameが20文字を超える場合
User オブジェクトは無効である
Finished in 0.11225 seconds (files took 0.4578 seconds to load)
3 examples, 0 failures
文字数のバリデーションと同じく、テストを先に追加しましょう。 nickname が空欄である User オブジェクトを作り、user.valid? をかけた時に空欄を許可しないことを示すエラーメッセージが含まれることを確認します。
spec/models/user_spec.rb
describe 'nickname存在性の検証' do
context 'nicknameが空欄の場合' do
let(:nickname) { '' }
it 'User オブジェクトは無効である' do
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include("can't be blank")
end
end
end
なお、上記テストの挿入位置ですが事前に追加していた describe '文字数制限の検証' do と同列となるように追記します。 これは、nickname 属性に関するいくつかの検証のうち「文字数制限」と「存在性」(空欄ではないこと) の検証が同列であることを明確にするためです。
$ bin/rspec spec/models/user_spec.rb
DEBUGGER: Attaching after process 50812 fork to child process 50861
Running via Spring preloader in process 50861
User
.first
事前に作成した通りのUserを返す
validation
nickname属性
文字数制限の検証
nicknameが20文字以下の場合
User オブジェクトは有効である
nicknameが20文字を超える場合
User オブジェクトは無効である
存在性の検証
nicknameが空欄の場合
User オブジェクトは無効である
Finished in 0.08105 seconds (files took 0.4687 seconds to load)
4 examples, 0 failures
require 'rails_helper'
describe User do
let(:nickname) { 'テスト太郎' }
let(:email) { 'test@example.com' }
let(:password) { '12345678' }
let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) } # 変数に格納
describe '.first' do
before do
create(:user, nickname: nickname, email: email)
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
end
describe 'validation' do
describe 'nickname属性' do
describe '文字数制限の検証' do
context 'nicknameが20文字以下の場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字
it 'User オブジェクトは有効である' do
expect(user.valid?).to be(true)
end
end
context 'nicknameが20文字を超える場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字
it 'User オブジェクトは無効である' do
user.valid?
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include('is too long (maximum is 20 characters)')
end
end
end
describe '存在性の検証' do
context 'nicknameが空欄の場合' do
let(:nickname) { '' }
it 'User オブジェクトは無効である' do
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include("can't be blank")
end
end
end
end
end
end
require 'rails_helper'
describe 'User', type: :system do
before { driven_by :selenium_chrome_headless }
# ユーザー情報入力用の変数
let(:email) { 'test@example.com' }
let(:nickname) { 'テスト太郎' }
let(:password) { 'password' }
let(:password_confirmation) { password }
describe 'ユーザー登録機能の検証' do
before { visit '/users/sign_up' }
# ユーザー登録を行う一連の操作を subject にまとめる
subject do
fill_in 'user_nickname', with: nickname
fill_in 'user_email', with: email
fill_in 'user_password', with: password
fill_in 'user_password_confirmation', with: password_confirmation
click_button 'ユーザー登録'
end
context '正常系' do
it 'ユーザーを作成できる' do
expect { subject }.to change(User, :count).by(1) # Userが1つ増える
expect(current_path).to eq('/') # ユーザー登録後はトップページにリダイレクト
end
end
end
end
ここまでで一旦、テストを実行しておきましょう。
$ bin/rspec spec/system/users_spec.rb
...
User
ユーザー登録機能の検証
正常系
DEBUGGER[rspec#59177]: Attaching after process 59169 fork to child process 59177
ユーザーを作成できる
Finished in 2.66 seconds (files took 0.71727 seconds to load)
1 example, 0 failures
require 'rails_helper'
describe 'User', type: :system do
before { driven_by :selenium_chrome_headless }
# ユーザー情報入力用の変数
let(:email) { 'test@example.com' }
let(:nickname) { 'テスト太郎' }
let(:password) { 'password' }
let(:password_confirmation) { password }
describe 'ユーザー登録機能の検証' do
before { visit '/users/sign_up' }
# ユーザー登録を行う一連の操作を subject にまとめる
subject do
fill_in 'user_nickname', with: nickname
fill_in 'user_email', with: email
fill_in 'user_password', with: password
fill_in 'user_password_confirmation', with: password_confirmation
click_button 'ユーザー登録'
end
context '正常系' do
it 'ユーザーを作成できる' do
expect { subject }.to change(User, :count).by(1) # Userが1つ増える
expect(current_path).to eq('/') # ユーザー登録後はトップページにリダイレクト
end
end
context '異常系' do
context 'nicknameが空の場合' do
let(:nickname) { '' }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count) # Userが増えない
expect(page).to have_content("Nickname can't be blank") # エラーメッセージのチェック
end
end
context 'nicknameが20文字を超える場合' do
let(:nickname) { 'あ' * 21 }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('Nickname is too long (maximum is 20 character')
end
end
context 'emailが空の場合' do
let(:email) { '' }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content("Email can't be blank")
end
end
context 'passwordが空の場合' do
let(:password) { '' }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content("Password can't be blank")
end
end
context 'passwordが6文字未満の場合' do
let(:password) { 'a' * 5 }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('Password is too short (minimum is 6 characters')
end
end
context 'passwordが128文字を超える場合' do
let(:password) { 'a' * 129 }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('Password is too long (maximum is 128 characters)')
end
end
context 'passwordとpassword_confirmationが一致しない場合' do
let(:password_confirmation) { "#{password}hoge" } # passwordに"hoge"を足した文字列にする
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content("Password confirmation doesn't match Password")
end
end
end
end
end
ユーザー認証機能関連の System Spec は前回作成した spec/system/users_spec.rb で扱っているので、ログイン機能のテスト機能についても同じファイルに追記します。
describe 'ユーザー登録機能の検証' のブロック (do ~ end) の下にテストを追加していきます。
spec/system/users_spec.rb
...
context 'passwordとpassword_confirmationが一致しない場合' do
let(:password_confirmation) { "#{password}hoge" } # passwordに"hoge"を足した文字列にする
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content("Password confirmation doesn't match Password")
end
end
end
end
######## ここから追記 #######
describe 'ログイン機能の検証' do
# 事前にユーザー作成
before do
create(:user, nickname: nickname, email: email, password: password, password_confirmation: password) # 事前にユーザー作成
visit '/users/sign_in'
fill_in 'user_email', with: email
fill_in 'user_password', with: 'password'
click_button 'ログイン'
end
context '正常系' do
it 'ログインに成功し、トップページにリダイレクトする' do
expect(current_path).to eq('/')
end
end
context '異常系' do
let(:password) { 'NGpassword' }
it 'ログインに失敗し、ページ遷移しない' do
expect(current_path).to eq('/users/sign_in')
end
end
end
テスト実行
では、すべてのテストに通ることを確認しましょう。
$ bin/rspec
...
ログイン機能の検証
正常系
ログインに成功し、トップページにリダイレクトする
異常系
ログインに失敗し、ページ遷移しない
Finished in 6.06 seconds (files took 0.42416 seconds to load)
16 examples, 0 failures
...
describe 'ログイン機能の検証' do
before do
create(:user, nickname: nickname, email: email, password: password, password_confirmation: password)
visit '/users/sign_in'
fill_in 'user_email', with: email
fill_in 'user_password', with: 'password'
click_button 'ログイン'
end
context '正常系' do
it 'ログインに成功し、トップページにリダイレクトする' do
expect(current_path).to eq('/')
end
it 'ログイン成功時のフラッシュメッセージを表示する' do # 追加
expect(page).to have_content('Signed in successfully')
end
end
context '異常系' do
let(:password) { 'NGpassword' }
it 'ログインに失敗し、ページ遷移しない' do
expect(current_path).to eq('/users/sign_in')
end
it 'ログイン失敗時のフラッシュメッセージを表示する' do # 追加
expect(page).to have_content('Invalid Email or password')
end
end
end
この時点では、まだフラッシュメッセージを実装していないためテストにコケることを確認してください。
$ bin/rspec
...
Finished in 10.2 seconds (files took 0.31626 seconds to load)
12 examples, 2 failures
Failed examples:
rspec ./spec/system/users_spec.rb:105 # User ログイン機能の検証 正常系 ログイン失敗時のフラッシュメッセージを表示する
rspec ./spec/system/users_spec.rb:116 # User ログイン機能の検証 異常系 ログイン失敗時のフラッシュメッセージを表示する
どの画面でも共通的に表示するものですので、どの画面の System Spec でチェックしても問題ありませんが、 例えばログイン画面はログインしているとトップページへリダイレクトされてしまうため、チェックには不向きです。
今回は既に作ってあるトップページ (Home#top) の System Spec を修正しましょう。
spec/system/home_spec.rb
require 'rails_helper'
RSpec.describe 'Home', type: :system do
before do
driven_by :selenium_chrome_headless
end
describe 'トップページアクセスの検証' do
it 'Home#top という文字列が表示される' do
visit '/'
expect(page).to have_content('Home#top')
end
end
######## ここから追加 #######
describe 'ナビゲーションバーの検証' do
context 'ログインしていない場合' do
before { visit '/' }
it 'ユーザー登録リンクを表示する' do
expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
end
it 'ログインリンクを表示する' do
expect(page).to have_link('ログイン', href: '/users/sign_in')
end
it 'ログアウトリンクは表示しない' do
expect(page).not_to have_content('ログアウト')
end
end
context 'ログインしている場合' do
before do
user = create(:user) # ログイン用のユーザーを作成
sign_in user # 作成したユーザーでログイン
visit '/'
end
it 'ユーザー登録リンクは表示しない' do
expect(page).not_to have_link('ユーザー登録', href: '/users/sign_up')
end
it 'ログインリンクは表示しない' do
expect(page).not_to have_link('ログイン', href: '/users/sign_in')
end
it 'ログアウトリンクを表示する' do
expect(page).to have_content('ログアウト')
end
it 'ログアウトリンクが機能する' do
click_button 'ログアウト'
# ログインしていない状態のリンク表示パターンになることを確認
expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
expect(page).to have_link('ログイン', href: '/users/sign_in')
expect(page).not_to have_content('ログアウト')
end
end
end
end
require 'rails_helper'
describe User do
let(:nickname) { 'テスト太郎' }
let(:email) { 'test@example.com' }
let(:user) { User.new(nickname: nickname, email: email, password: password, password_confirmation: password) } # 変数に格納
describe '.first' do
before do
create(:user, nickname: nickname, email: email)
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
end
describe 'validation' do
let(:password) { '12345678' }
describe 'nickname属性' do
describe '文字数制限の検証' do
context 'nicknameが20文字以下の場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてと' } # 20文字
it 'User オブジェクトは有効である' do
expect(user.valid?).to be(true)
end
end
context 'nicknameが20文字を超える場合' do
let(:nickname) { 'あいうえおかきくけこさしすせそたちつてとな' } # 21文字
it 'User オブジェクトは無効である' do
user.valid?
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include('は20文字以下に設定して下さい。')
end
end
end
describe '存在性の検証' do
context 'nicknameが空欄の場合' do
let(:nickname) { '' }
it 'User オブジェクトは無効である' do
expect(user.valid?).to be(false)
expect(user.errors[:nickname]).to include("が入力されていません。")
end
end
end
end
end
end
spec/system/users_spec.rb
require 'rails_helper'
describe 'User', type: :system do
before { driven_by :selenium_chrome_headless }
# ユーザー情報入力用の変数
let(:email) { 'test@example.com' }
let(:nickname) { 'テスト太郎' }
let(:password) { 'password' }
let(:password_confirmation) { password }
describe 'ユーザー登録機能の検証' do
before { visit '/users/sign_up' }
# ユーザー登録を行う一連の操作を subject にまとめる
subject do
fill_in 'user_nickname', with: nickname
fill_in 'user_email', with: email
fill_in 'user_password', with: password
fill_in 'user_password_confirmation', with: password_confirmation
click_button 'ユーザー登録'
end
context '正常系' do
it 'ユーザーを作成できる' do
expect { subject }.to change(User, :count).by(1) # Userが1つ増える
expect(page).to have_content('ユーザー登録に成功しました。')
expect(current_path).to eq('/') # ユーザー登録後はトップページにリダイレクト
end
end
context '異常系' do
context 'エラー理由が1件の場合' do
let(:nickname) { '' }
it 'ユーザー作成に失敗した旨のエラーメッセージを表示する' do
subject
expect(page).to have_content('エラーが発生したためユーザーは保存されませんでした。')
end
end
context 'エラー理由が2件以上の場合' do
let(:nickname) { '' }
let(:email) { '' }
it '問題件数とともに、ユーザー作成に失敗した旨のエラーメッセージを表示する' do
subject
expect(page).to have_content('エラーが発生したためユーザーは保存されませんでした。')
end
end
context 'nicknameが空の場合' do
let(:nickname) { '' }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count) # Userが増えない
expect(page).to have_content('ニックネーム が入力されていません。') # エラーメッセージのチェック
end
end
context 'nicknameが20文字を超える場合' do
let(:nickname) { 'あ' * 21 }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('ニックネーム は20文字以下に設定して下さい。')
end
end
context 'emailが空の場合' do
let(:email) { '' }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('メールアドレス が入力されていません。')
end
end
context 'passwordが空の場合' do
let(:password) { '' }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('パスワード が入力されていません。')
end
end
context 'passwordが6文字未満の場合' do
let(:password) { 'a' * 5 }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('パスワード は6文字以上に設定して下さい。')
end
end
context 'passwordが128文字を超える場合' do
let(:password) { 'a' * 129 }
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('パスワード は128文字以下に設定して下さい。')
end
end
context 'passwordとpassword_confirmationが一致しない場合' do
let(:password_confirmation) { "#{password}hoge" } # passwordに"hoge"を足した文字列にする
it 'ユーザーを作成せず、エラーメッセージを表示する' do
expect { subject }.not_to change(User, :count)
expect(page).to have_content('確認用パスワード が一致していません。')
end
end
end
end
describe 'ログイン機能の検証' do
before do
create(:user, nickname: nickname, email: email, password: password, password_confirmation: password) # ユーザー作成
visit '/users/sign_in'
fill_in 'user_email', with: email
fill_in 'user_password', with: 'password'
click_button 'ログイン'
end
context '正常系' do
it 'ログインに成功し、トップページにリダイレクトする' do
expect(current_path).to eq('/')
end
it 'ログイン成功時のフラッシュメッセージを表示する' do
expect(page).to have_content('ログインしました。')
end
end
context '異常系' do
let(:password) { 'NGpassword' }
it 'ログインに失敗し、ページ遷移しない' do
expect(current_path).to eq('/users/sign_in')
end
it 'ログイン失敗時のフラッシュメッセージを表示する' do
expect(page).to have_content('メールアドレスまたはパスワードが違います。')
end
end
end
describe 'ログアウト機能の検証' do
before do
user = create(:user, nickname: nickname, email: email, password: password, password_confirmation: password) # ユーザー作成
sign_in user # 作成したユーザーでログイン
visit '/'
click_button 'ログアウト'
end
it 'トップページにリダイレクトする' do
expect(current_path).to eq('/')
end
it 'ログアウト時のフラッシュメッセージを表示する' do
expect(page).to have_content('ログアウトしました。')
end
end
end
$ bin/rspec
...
Failures:
1) Home ナビゲーションバーの検証 ログインしている場合 ログアウトリンクが機能する
Failure/Error: expect(page).not_to have_content('ログアウト')
expected not to find text "ログアウト" in "TechLog\nユーザー登録\nログイン\nログアウトしました。\nHome#top\nFind me in app/views/home/top.html.erb"
...
Failed examples:
rspec ./spec/system/home_spec.rb:52 # Home ナビゲーションバーの検証 ログインしている場合 ログアウトリンクが機能する
ナビゲーションバーを追加した時のテストがコケてしまいました。
テストのエラーメッセージをチェックしてみます。
Failure/Error: expect(page).not_to have_content('ログアウト')
expected not to find text "ログアウト" in "TechLog\nユーザー登録\nログイン\nログアウトしました。\nHome#top\nFind me in app/views/home/top.html.erb"
expected not to find text "ログアウト"... という一文に注目してください。 「ログアウトというテキストが含まれないことを期待していたが、実際には含まれていた」というエラーです。
ここでテストの中身を確認してみます。
spec/system/home_spec.rb
...
it 'ログアウトリンクが機能する' do
click_button 'ログアウト'
# ログインしていない状態のリンク表示パターンになることを確認
expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
expect(page).to have_link('ログイン', href: '/users/sign_in')
expect(page).not_to have_content('ログアウト')****
end
...
Post モデルに関するテストをスムーズに作成するため、FactoryBot を用いてテスト内で Post を準備しやすくしておきます。 rails g コマンドで作成された spec/factories/post.rb を以下のように編集してください。
spec/factories/post.rb
FactoryBot.define do
factory :post do
title { 'タイトル1' }
content { '本文1' }
association :user, factory: :user
end
end
association :user, factory: :user の1行により、同時に Post に紐づく User も作成できるように設定していることに注意してください。
これで、RSpec ファイル内で create(:post) という1行だけで Post を作成できるようになりました。 次は、実際に Post モデルのテストを準備しましょう。
Post モデルのテスト
spec/models/post_spec.rb
require 'rails_helper'
describe Post do
before { @user = create(:user) } # 事前にユーザーを作成
let(:title) { 'テストタイトル' }
let(:content) { 'テスト本文' }
let(:user_id) { @user.id } # 作成したユーザーのIDを外部キーに設定
describe 'バリデーションの検証' do
let(:post) { Post.new(title: title, content: content, user_id: user_id) }
context '正常系' do
it '有効である' do
expect(post.valid?).to be(true)
end
end
context '異常系' do
context 'titleが空の場合' do
let(:title) { nil }
it '無効である' do
expect(post.valid?).to be(false)
expect(post.errors[:title]).to include('が入力されていません。')
end
end
context 'titleが100文字を超える場合' do
let(:title) { 'あ' * 101 }
it '無効である' do
expect(post.valid?).to be(false)
end
end
context 'contentが空の場合' do
let(:content) { nil }
it '無効である' do
expect(post.valid?).to be(false)
expect(post.errors[:content]).to include('が入力されていません。')
end
end
context 'contentが1000文字を超える場合' do
let(:content) { 'あ' * 1001 }
it '無効である' do
expect(post.valid?).to be(false)
end
end
context 'user_idが空の場合' do
let(:user_id) { nil }
it '無効である' do
expect(post.valid?).to be(false)
expect(post.errors[:user]).to include('が入力されていません。')
end
end
end
end
describe 'Postが持つ情報の検証' do
before { create(:post, title: title, content: content, user_id: user_id) } # Post を作成
subject { described_class.first }
it 'Postの属性値を返す' do
expect(subject.title).to eq('テストタイトル')
expect(subject.content).to eq('テスト本文')
expect(subject.user_id).to eq(@user.id)
end
end
end
以上が Post モデルのテストです。 バリデーションのエラーメッセージは日本語ですので、後ほど翻訳ファイル (config/locales/devise.views.ja.yml) も修正する必要があります。
User モデルのテスト
次は User モデルに紐づく Post を取得できる仕様を満たすテストを追加します。
User モデルのテストは作成済みですので、その中にテストを追加・修正してあげます。
spec/models/user_spec.rb
...
describe '.first' do
before do
@user = create(:user, nickname: nickname, email: email) # 修正
@post = create(:post, title: 'タイトル', content: '本文', user_id: @user.id) # 修正
end
subject { described_class.first }
it '事前に作成した通りのUserを返す' do
expect(subject.nickname).to eq('テスト太郎')
expect(subject.email).to eq('test@example.com')
end
######## ここから追加 #######
it '紐づくPostの情報を取得できる' do
expect(subject.posts.size).to eq(1)
expect(subject.posts.first.title).to eq('タイトル')
expect(subject.posts.first.content).to eq('本文')
expect(subject.posts.first.user_id).to eq(@user.id)
end
######## ここまで追加 #######
end
...
before ブロックの中で、User に紐づく Post を作成しています。
また、User は複数の Post を持つ場合があるので User.post ではなく User.posts と、複数の Post を取得することに注意してください。
モデルの修正
では、テストが通るように Post モデルと User モデルを修正していきましょう.
Post モデルの修正
Post モデルを定義するファイルを以下のように修正します。
app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true, length: { maximum: 100 }
validates :content, presence: true, length: { maximum: 1000 }
end
belongs_to により、Post に対して単一の User を紐付けています。
User モデルの修正
次に User モデルを定義するファイルを以下のように修正します。
app/models/user.rb
class User < ApplicationRecord
has_many :posts # 追加
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validates :nickname, presence: true, length: { maximum: 20 }
end
まずは、新しく作った投稿ページへのアクセスを確認する Request Spec を追加していきます。 Request Spec のテストファイル自体は rails g コマンド実行時に生成されているので、中身を編集するだけで OK です。
spec/requests/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Posts', type: :request do
before { @user = create(:user) } # 各テストで使用できるユーザーを作成
describe 'GET /posts/new' do
context 'ログインしていない場合' do
it 'HTTPステータス302を返す' do
get '/posts/new'
expect(response).to have_http_status(302)
end
it 'ログインページにリダイレクトされる' do
get '/posts/new'
expect(response).to redirect_to '/users/sign_in'
end
end
context 'ログインしている場合' do
before { sign_in @user }
it 'HTTPステータス200を返す' do
get '/posts/new'
expect(response).to have_http_status(200)
end
it 'ログインページにリダイレクトされない' do
get '/posts/new'
expect(response).not_to redirect_to '/users/sign_in'
end
end
end
end
$ bundle exec rspec spec/requests/posts_spec.rb
Posts
GET /posts/new
ログインしていない場合
HTTPステータス302を返す
ログインページにリダイレクトされる
ログインしている場合
HTTPステータス200を返す
ログインページにリダイレクトされない
Finished in 0.20681 seconds (files took 1.33 seconds to load)
4 examples, 0 failures
これで Request Spec は完了です。 次は System Spec を作成してあげましょう。
System Spec
System Spec は自動では作られていないので、まずはファイル自体を作成します。
$ touch spec/system/posts_spec.rb
作った System Spec の中身は以下のようにします。 基本的にはブラウザで動作確認した時と同一の内容をテストしています。
spec/system/posts_spec.rb
require 'rails_helper'
describe 'Post', type: :system do
before do
driven_by :selenium_chrome_headless # ヘッドレスモードで実行
@user = create(:user) # ログイン用ユーザー作成
end
# 投稿フォーム
let(:title) { 'テストタイトル' }
let(:content) { 'テスト本文' }
describe 'ログ投稿機能の検証' do
# ログ投稿を行う一連の操作を subject にまとめる
subject do
fill_in 'post_title', with: title
fill_in 'post_content', with: content
click_button 'ログを記録'
end
context 'ログインしていない場合' do
before { visit '/posts/new' }
it 'ログインページへリダイレクトする' do
expect(current_path).to eq('/users/sign_in')
expect(page).to have_content('ログインしてください。')
end
end
context 'ログインしている場合' do
before do
sign_in @user
visit '/posts/new'
end
it 'ログインページへリダイレクトしない' do
expect(current_path).not_to eq('/users/sign_in')
end
context 'パラメータが正常な場合' do
it 'Postを作成できる' do
expect { subject }.to change(Post, :count).by(1)
expect(current_path).to eq('/')
expect(page).to have_content('投稿しました')
end
end
context 'パラメータが異常な場合' do
let(:title) { nil }
it 'Postを作成できない' do
expect { subject }.not_to change(Post, :count)
expect(page).to have_content('投稿に失敗しました')
end
it '入力していた内容は維持される' do
subject
expect(page).to have_field('post_content', with: content)
end
end
end
end
end
System Spec (Home)
今回、ナビゲーションバーにリンクを追加しました。 ナビゲーションバーのリンク表示のテストは Home の System Spec で行っていたため、テストを追加しておきます。
以下では、追記部分を中心に表記しているので注意してください。
spec/system/home_spec.rb
...
describe 'ナビゲーションバーの検証' do
context 'ログインしていない場合' do
...
it 'ログ投稿リンクを表示しない' do # 追加
expect(page).not_to have_link('ログ投稿', href: '/posts/new')
end
it 'ログアウトリンクは表示しない' do
expect(page).not_to have_content('ログアウト')
end
...
context 'ログインしている場合' do
...
it 'ログ投稿リンクを表示する' do # 追加
expect(page).to have_link('ログ投稿', href: '/posts/new')
end
it 'ログアウトリンクを表示する' do
expect(page).to have_content('ログアウト')
end
...
最後に、すべてのテストに通ることを確認しておきます。
$ bin/rspec
...
Finished in 11.06 seconds (files took 0.38787 seconds to load)
48 examples, 0 failures
Rails.application.routes.draw do
devise_for :users
root 'home#top'
resources :posts, only: [:new, :create, :show] # 追加
end
これで、/posts/:id というパスへアクセスすると学習ログの詳細ページのアクションへ飛ぶようになります。 ページを表示するためには show アクションが必要となるので Controller にアクションと、対応する View を追加していきましょう。
Controller (#show)
PostsController に show アクションを用意してあげます。 外部から呼び出すアクション (メソッド )なので、private の上に書くことに注意してください。
app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!
...
def show # 追加
@post = Post.find_by(id: params[:id])
end
private
def post_params
params.require(:post).permit(:title, :content)
end
end
/posts/:id の :id 部分をパラメータとして受け取り、該当する Post を取得しています。
また、TechLog では投稿の閲覧についてはログインを必要としないため、authenticate_user!から show アクションは対象外とします。
まずは、新しく作った詳細ページへのアクセスを確認する Request Spec を追加していきます。 今回は事前に Post が一つ以上は存在している必要があるため、Post の FactoryBot を使っていることに注意しましょう。
spec/requests/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Posts', type: :request do
before do # beforeブロックを修正
@user = create(:user)
@post = create(:post) # 追加
end
...
describe 'GET /posts/:id' do
context 'ログインしていない場合' do
it 'HTTPステータス200を返す' do
get "/posts/#{@post.id}"
expect(response).to have_http_status '200'
end
end
context 'ログインしている場合' do
it 'HTTPステータス200を返す' do
sign_in @user
get "/posts/#{@post.id}"
expect(response).to have_http_status '200'
end
end
end
Request Spec の全文は以下のようになります。
spec/requests/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Posts', type: :request do
before do
@user = create(:user) # 各テストで使用できるユーザーを作成
@post = create(:post) # 閲覧用の Post を作成
end
describe 'GET /posts/new' do
context 'ログインしていない場合' do
it 'HTTPステータス302を返す' do
get '/posts/new'
expect(response).to have_http_status(302)
end
it 'ログインページにリダイレクトされる' do
get '/posts/new'
expect(response).to redirect_to '/users/sign_in'
end
end
context 'ログインしている場合' do
before { sign_in @user }
it 'HTTPステータス200を返す' do
get '/posts/new'
expect(response).to have_http_status(200)
end
it 'ログインページにリダイレクトされない' do
get '/posts/new'
expect(response).not_to redirect_to '/users/sign_in'
end
end
end
describe 'GET /posts/:id' do
context 'ログインしていない場合' do
it 'HTTPステータス200を返す' do
get "/posts/#{@post.id}"
expect(response).to have_http_status '200'
end
end
context 'ログインしている場合' do
it 'HTTPステータス200を返す' do
sign_in @user
get "/posts/#{@post.id}"
expect(response).to have_http_status '200'
end
end
end
end
ログイン状態によらずリダイレクトされずアクセスできることを含め、アクセス性を確認するテストを作成しました。 テストを実行してみてください。 すべて成功すれば OK です。
$ bin/rspec spec/requests/posts_spec.rb
DEBUGGER: Attaching after process 8205 fork to child process 10953
Running via Spring preloader in process 10953
Posts
GET /posts/new
ログインしていない場合
HTTPステータス302を返す
ログインページにリダイレクトされる
ログインしている場合
HTTPステータス200を返す
ログインページにリダイレクトされない
GET /posts/:id
ログインしていない場合
HTTPステータス200を返す
ログインしている場合
HTTPステータス200を返す
Finished in 0.51404 seconds (files took 0.70362 seconds to load)
6 examples, 0 failures
これで Request Spec は完了です。 次は System Spec を作成してあげましょう。
System Spec
こちらも Factory Bot を用いて事前に Post を1つ作成してから、詳細ページの表示内容を確認するテストにします。 System Spec に以下のテストを追記します。
spec/system/posts_spec.rb
require 'rails_helper'
describe 'Post', type: :system do
before do
driven_by :selenium_chrome_headless # ヘッドレスモードで実行
@user = create(:user) # ログイン用ユーザー作成
@post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id) # 追加
end
...
####### ここから追加 #######
describe 'ログ詳細機能の検証' do
before { visit "/posts/#{@post.id}" }
it 'Postの詳細が表示される' do
expect(page).to have_content('RSpec学習完了')
expect(page).to have_content('System Specを作成した')
expect(page).to have_content(@user.nickname)
end
end
end
この時点での System Spec の全文は以下の通りです。
spec/system/posts_spec.rb
require 'rails_helper'
describe 'Post', type: :system do
before do
driven_by :selenium_chrome_headless # ヘッドレスモードで実行
@user = create(:user) # ログイン用ユーザー作成
@post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id)
end
# 投稿フォーム
let(:title) { 'テストタイトル' }
let(:content) { 'テスト本文' }
describe 'ログ投稿機能の検証' do
# ログ投稿を行う一連の操作を subject にまとめる
subject do
fill_in 'post_title', with: title
fill_in 'post_content', with: content
click_button 'ログを記録'
end
context 'ログインしていない場合' do
before { visit '/posts/new' }
it 'ログインページへリダイレクトする' do
expect(current_path).to eq('/users/sign_in')
expect(page).to have_content('ログインしてください。')
end
end
context 'ログインしている場合' do
before do
sign_in @user
visit '/posts/new'
end
it 'ログインページへリダイレクトしない' do
expect(current_path).not_to eq('/users/sign_in')
end
context 'パラメータが正常な場合' do
it 'Postを作成できる' do
expect { subject }.to change(Post, :count).by(1)
expect(current_path).to eq('/')
expect(page).to have_content('投稿しました')
end
end
context 'パラメータが異常な場合' do
let(:title) { nil }
it 'Postを作成できない' do
expect { subject }.not_to change(Post, :count)
expect(page).to have_content('投稿に失敗しました')
end
it '入力していた内容は維持される' do
subject
expect(page).to have_field('post_content', with: content)
end
end
end
end
describe 'ログ詳細機能の検証' do
before { visit "/posts/#{@post.id}" }
it 'Postの詳細が表示される' do
expect(page).to have_content('RSpec学習完了')
expect(page).to have_content('System Specを作成した')
expect(page).to have_content(@user.nickname)
end
end
end
テスト実行
最後に、すべてのテストに通ることを確認しておきます。
$ bin/rspec
...
Finished in 12 seconds (files took 0.69987 seconds to load)
51 examples, 0 failures
...
describe 'ログ投稿機能の検証' do
...
context 'ログインしている場合' do
before do
sign_in @user
visit '/posts/new'
end
it 'ログインページへリダイレクトしない' do
expect(current_path).not_to eq('/users/sign_in')
end
context 'パラメータが正常な場合' do
it 'Postを作成できる' do
expect { subject }.to change(Post, :count).by(1)
expect(current_path).to eq('/posts') # 修正
...
describe 'GET /posts' do
context 'ログインしていない場合' do
it 'HTTPステータス200を返す' do
get '/posts'
expect(response).to have_http_status '200'
end
end
context 'ログインしている場合' do
it 'HTTPステータス200を返す' do
sign_in @user
get '/posts'
expect(response).to have_http_status '200'
end
end
end
...
Request Spec の全文は以下のようになります。
spec/requests/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Posts', type: :request do
before do
@user = create(:user) # 各テストで使用できるユーザーを作成
@post = create(:post) # 閲覧用の Post を作成
end
describe 'GET /posts/new' do
context 'ログインしていない場合' do
it 'HTTPステータス302を返す' do
get '/posts/new'
expect(response).to have_http_status(302)
end
it 'ログインページにリダイレクトされる' do
get '/posts/new'
expect(response).to redirect_to '/users/sign_in'
end
end
context 'ログインしている場合' do
before { sign_in @user }
it 'HTTPステータス200を返す' do
get '/posts/new'
expect(response).to have_http_status(200)
end
it 'ログインページにリダイレクトされない' do
get '/posts/new'
expect(response).not_to redirect_to '/users/sign_in'
end
end
end
describe 'GET /posts/:id' do
context 'ログインしていない場合' do
it 'HTTPステータス200を返す' do
get "/posts/#{@post.id}"
expect(response).to have_http_status '200'
end
end
context 'ログインしている場合' do
it 'HTTPステータス200を返す' do
sign_in @user
get "/posts/#{@post.id}"
expect(response).to have_http_status '200'
end
end
end
describe 'GET /posts' do
context 'ログインしていない場合' do
it 'HTTPステータス200を返す' do
get '/posts'
expect(response).to have_http_status '200'
end
end
context 'ログインしている場合' do
it 'HTTPステータス200を返す' do
sign_in @user
get '/posts'
expect(response).to have_http_status '200'
end
end
end
end
これで Request Spec は完了です。 次は System Spec を作成してあげましょう。
System Spec
Factory Bot を用いて事前に Post を2つ作成してから、一覧ページの表示内容を確認するテストにします。 System Spec に以下のテストを追記します。
spec/system/posts_spec.rb
require 'rails_helper'
describe 'Post', type: :system do
before do
driven_by :selenium_chrome_headless # ヘッドレスモードで実行
@user = create(:user) # ログイン用ユーザー作成
@post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id)
@post2 = create(:post, title: 'RSpec学習完了 2', content: 'System Specを作成した 2', user_id: @user.id) # 追加
end
...
####### ここから追加 #######
describe 'ログ一覧機能の検証' do
before { visit '/posts' }
it '1件目のPostの詳細が表示される' do
expect(page).to have_content('RSpec学習完了')
expect(page).to have_content('System Specを作成した')
expect(page).to have_content(@user.nickname)
end
it '2件目のPostの詳細が表示される' do
expect(page).to have_content('RSpec学習完了 2')
expect(page).to have_content('System Specを作成した 2')
expect(page).to have_content(@user.nickname)
end
it '投稿タイトルをクリックすると詳細ページへ遷移する' do
click_link 'RSpec学習完了'
expect(current_path).to eq("/posts/#{@post.id}")
end
end
end
この時点での System Spec の全文は以下の通りです。
spec/system/posts_spec.rb
require 'rails_helper'
describe 'Post', type: :system do
before do
driven_by :selenium_chrome_headless # ヘッドレスモードで実行
@user = create(:user) # ログイン用ユーザー作成
@post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id)
@post2 = create(:post, title: 'RSpec学習完了 2', content: 'System Specを作成した 2', user_id: @user.id)
end
# 投稿フォーム
let(:title) { 'テストタイトル' }
let(:content) { 'テスト本文' }
describe 'ログ投稿機能の検証' do
# ログ投稿を行う一連の操作を subject にまとめる
subject do
fill_in 'post_title', with: title
fill_in 'post_content', with: content
click_button 'ログを記録'
end
context 'ログインしていない場合' do
before { visit '/posts/new' }
it 'ログインページへリダイレクトする' do
expect(current_path).to eq('/users/sign_in')
expect(page).to have_content('ログインしてください。')
end
end
context 'ログインしている場合' do
before do
sign_in @user
visit '/posts/new'
end
it 'ログインページへリダイレクトしない' do
expect(current_path).not_to eq('/users/sign_in')
end
context 'パラメータが正常な場合' do
it 'Postを作成できる' do
expect { subject }.to change(Post, :count).by(1)
expect(current_path).to eq('/posts')
expect(page).to have_content('投稿しました')
end
end
context 'パラメータが異常な場合' do
let(:title) { nil }
it 'Postを作成できない' do
expect { subject }.not_to change(Post, :count)
expect(page).to have_content('投稿に失敗しました')
end
it '入力していた内容は維持される' do
subject
expect(page).to have_field('post_content', with: content)
end
end
end
end
describe 'ログ詳細機能の検証' do
before { visit "/posts/#{@post.id}" }
it 'Postの詳細が表示される' do
expect(page).to have_content('RSpec学習完了')
expect(page).to have_content('System Specを作成した')
expect(page).to have_content(@user.nickname)
end
end
describe 'ログ一覧機能の検証' do
before { visit '/posts' }
it '1件目のPostの詳細が表示される' do
expect(page).to have_content('RSpec学習完了')
expect(page).to have_content('System Specを作成した')
expect(page).to have_content(@user.nickname)
end
it '2件目のPostの詳細が表示される' do
expect(page).to have_content('RSpec学習完了 2')
expect(page).to have_content('System Specを作成した 2')
expect(page).to have_content(@user.nickname)
end
it '投稿タイトルをクリックすると詳細ページへ遷移する' do
click_link 'RSpec学習完了'
expect(current_path).to eq("/posts/#{@post.id}")
end
end
end
テスト実行
最後に、すべてのテストに通ることを確認しておきます。
$ bin/rspec
...
Finished in 12.21 seconds (files took 0.41113 seconds to load)
58 examples, 0 failures
次は投稿したユーザー以外の表示です。 別なユーザーを作ってログイン、またはログアウトし、学習ログ一覧から任意の投稿の詳細画面にアクセスしてください。 削除ボタンが表示されていなければ OK です。
テスト
それでは、削除機能に関するテストを追加しましょう。 先ほど動作確認した内容を System Spec に追加します。
...
describe 'ログ削除機能の検証' do
context '投稿したユーザーでログインしている場合' do
before do
sign_in @user
visit "/posts/#{@post.id}"
end
it '削除ボタンを表示する' do
expect(page).to have_button('削除')
end
it '削除ボタンをクリックすると削除できる' do
expect do
click_button '削除'
end.to change(Post, :count).by(-1) # 削除ボタンをクリックするとPostが1つ減る
# リダイレクト後の画面確認
expect(current_path).to eq('/posts')
expect(page).to have_content('投稿が削除されました') # フラッシュメッセージを表示
expect(page).not_to have_link("/posts/#{@post.id}") # 削除した投稿(の詳細ページへのリンク)が存在しない
end
end
context '投稿したユーザーでログインしていない場合' do
it '削除ボタンを表示しない' do
visit "/posts/#{@post.id}"
expect(page).not_to have_button('削除')
end
it '直接リクエストを投げても削除されない' do
visit "/posts/#{@post.id}"
expect do
delete post_path(@post) # 投稿データを削除するリクエストを送る
end.not_to change(Post, :count)
end
end
end
end
最後にすべてのテストを実行し、通ることを確認します。
$ bin/rspec
...
Finished in 13.55 seconds (files took 0.41183 seconds to load)
62 examples, 0 failures
describe 'GET /posts' do
context 'ログインしていない場合' do
it 'HTTPステータス200を返す' do
get '/posts'
expect(response).to have_http_status '200'
end
end
context 'ログインしている場合' do
it 'HTTPステータス200を返す' do
sign_in @user
get '/posts'
expect(response).to have_http_status '200'
end
end
end
変更後: spec/requests/posts_spec.rb
describe 'GET /' do # 修正
context 'ログインしていない場合' do
it 'HTTPステータス200を返す' do
get '/' # 修正
expect(response).to have_http_status '200'
end
end
context 'ログインしている場合' do
it 'HTTPステータス200を返す' do
sign_in @user
get '/' # 修正
expect(response).to have_http_status '200'
end
end
end
System Spec
次は System Spec に変更を行います。
ナビゲーションバーのテスト移行
home の System Spec には、ナビゲーションバーに関する以下のテストが含まれていました。
spec/system/home_spec.rb
...
describe 'ナビゲーションバーの検証' do
context 'ログインしていない場合' do
before { visit '/' }
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/posts')
end
it 'ユーザー登録リンクを表示する' do
expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
end
it 'ログインリンクを表示する' do
expect(page).to have_link('ログイン', href: '/users/sign_in')
end
it 'ログ投稿リンクを表示しない' do
expect(page).not_to have_link('ログ投稿', href: '/posts/new')
end
it 'ログアウトリンクは表示しない' do
expect(page).not_to have_content('ログアウト')
end
end
context 'ログインしている場合' do
before do
user = create(:user) # ログイン用のユーザーを作成
sign_in user # 作成したユーザーでログイン
visit '/'
end
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/posts')
end
it 'ユーザー登録リンクは表示しない' do
expect(page).not_to have_link('ユーザー登録', href: '/users/sign_up')
end
it 'ログインリンクは表示しない' do
expect(page).not_to have_link('ログイン', href: '/users/sign_in')
end
it 'ログ投稿リンクを表示する' do
expect(page).to have_link('ログ投稿', href: '/posts/new')
end
it 'ログアウトリンクを表示する' do
expect(page).to have_content('ログアウト')
end
it 'ログアウトリンクが機能する' do
click_button 'ログアウト'
# ログインしていない状態のリンク表示パターンになることを確認
expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
expect(page).to have_link('ログイン', href: '/users/sign_in')
expect(page).not_to have_button('ログアウト')
end
end
end
end
ログイン状態に応じたナビゲーションバーの表示のテストを home の System Spec にまとめて書いていましたが、 中身は「ユーザー機能」と「ログ投稿機能」に関するテストが混在していました。
この機会に、それぞれ users の System Spec と posts の System Spec に移行しましょう。 home の System Spec をそれぞれの System Spec にコピペで貼り付け、それぞれの機能に関する箇所だけを残すようにすると移行が楽です。
spec/system/posts_spec.rb
...
#### ここから追加 ####
describe 'ナビゲーションバーの検証' do
context 'ログインしていない場合' do
before { visit '/' }
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/posts')
end
it 'ログ投稿リンクを表示しない' do
expect(page).not_to have_link('ログ投稿', href: '/posts/new')
end
end
context 'ログインしている場合' do
before do
user = create(:user) # ログイン用のユーザーを作成
sign_in user # 作成したユーザーでログイン
visit '/'
end
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/posts')
end
it 'ログ投稿リンクを表示する' do
expect(page).to have_link('ログ投稿', href: '/posts/new')
end
end
end
#### ここまで追加 ####
spec/system/users_spec.rb
...
#### ここから追加 ####
describe 'ナビゲーションバーの検証' do
context 'ログインしていない場合' do
before { visit '/' }
it 'ユーザー登録リンクを表示する' do
expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
end
it 'ログインリンクを表示する' do
expect(page).to have_link('ログイン', href: '/users/sign_in')
end
it 'ログアウトリンクは表示しない' do
expect(page).not_to have_content('ログアウト')
end
end
context 'ログインしている場合' do
before do
user = create(:user) # ログイン用のユーザーを作成
sign_in user # 作成したユーザーでログイン
visit '/'
end
it 'ユーザー登録リンクは表示しない' do
expect(page).not_to have_link('ユーザー登録', href: '/users/sign_up')
end
it 'ログインリンクは表示しない' do
expect(page).not_to have_link('ログイン', href: '/users/sign_in')
end
it 'ログアウトリンクを表示する' do
expect(page).to have_content('ログアウト')
end
it 'ログアウトリンクが機能する' do
click_button 'ログアウト'
# ログインしていない状態のリンク表示パターンになることを確認
expect(page).to have_link('ユーザー登録', href: '/users/sign_up')
expect(page).to have_link('ログイン', href: '/users/sign_in')
expect(page).not_to have_button('ログアウト')
end
end
end
#### ここまで追加 ####
posts#index のルーティング
Post の System Spec についてですが、投稿一覧のパスは /posts ではなく / になったので修正しておきましょう。
spec/system/posts_spec.rb
require 'rails_helper'
describe 'Post', type: :system do
...
describe 'ログ投稿機能の検証' do
...
context 'ログインしている場合' do
...
context 'パラメータが正常な場合' do
it 'Postを作成できる' do
expect { subject }.to change(Post, :count).by(1)
expect(current_path).to eq('/') # 修正
...
describe 'ログ一覧機能の検証' do
before { visit '/' } # 修正
...
describe 'ログ削除機能の検証' do
context '投稿したユーザーでログインしている場合' do
...
it '削除ボタンをクリックすると削除できる' do
...
# リダイレクト後の画面確認
expect(current_path).to eq('/') # 修正
...
describe 'ナビゲーションバーの検証' do
context 'ログインしていない場合' do
before { visit '/' }
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/') # 修正
end
...
context 'ログインしている場合' do
...
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/posts') # 修正
...
修正後の Post System Spec の全文は以下のようになります。
spec/system/posts_spec.rb
require 'rails_helper'
describe 'Post', type: :system do
before do
driven_by :selenium_chrome_headless # ヘッドレスモードで実行
@user = create(:user) # ログイン用ユーザー作成
@post = create(:post, title: 'RSpec学習完了', content: 'System Specを作成した', user_id: @user.id)
@post2 = create(:post, title: 'RSpec学習完了 2', content: 'System Specを作成した 2', user_id: @user.id)
end
# 投稿フォーム
let(:title) { 'テストタイトル' }
let(:content) { 'テスト本文' }
describe 'ログ投稿機能の検証' do
# ログ投稿を行う一連の操作を subject にまとめる
subject do
fill_in 'post_title', with: title
fill_in 'post_content', with: content
click_button 'ログを記録'
end
context 'ログインしていない場合' do
before { visit '/posts/new' }
it 'ログインページへリダイレクトする' do
expect(current_path).to eq('/users/sign_in')
expect(page).to have_content('ログインしてください。')
end
end
context 'ログインしている場合' do
before do
sign_in @user
visit '/posts/new'
end
it 'ログインページへリダイレクトしない' do
expect(current_path).not_to eq('/users/sign_in')
end
context 'パラメータが正常な場合' do
it 'Postを作成できる' do
expect { subject }.to change(Post, :count).by(1)
expect(current_path).to eq('/')
expect(page).to have_content('投稿しました')
end
end
context 'パラメータが異常な場合' do
let(:title) { nil }
it 'Postを作成できない' do
expect { subject }.not_to change(Post, :count)
expect(page).to have_content('投稿に失敗しました')
end
it '入力していた内容は維持される' do
subject
expect(page).to have_field('post_content', with: content)
end
end
end
end
describe 'ログ詳細機能の検証' do
before { visit "/posts/#{@post.id}" }
it 'Postの詳細が表示される' do
expect(page).to have_content('RSpec学習完了')
expect(page).to have_content('System Specを作成した')
expect(page).to have_content(@user.nickname)
end
end
describe 'ログ一覧機能の検証' do
before { visit '/posts' }
it '1件目のPostの詳細が表示される' do
expect(page).to have_content('RSpec学習完了')
expect(page).to have_content('System Specを作成した')
expect(page).to have_content(@user.nickname)
end
it '2件目のPostの詳細が表示される' do
expect(page).to have_content('RSpec学習完了 2')
expect(page).to have_content('System Specを作成した 2')
expect(page).to have_content(@user.nickname)
end
it '投稿タイトルをクリックすると詳細ページへ遷移する' do
click_link 'RSpec学習完了'
expect(current_path).to eq("/posts/#{@post.id}")
end
end
describe 'ログ削除機能の検証' do
context '投稿したユーザーでログインしている場合' do
before do
sign_in @user
visit "/posts/#{@post.id}"
end
it '削除ボタンを表示する' do
expect(page).to have_button('削除')
end
it '削除ボタンをクリックすると削除できる' do
expect do
click_button '削除'
end.to change(Post, :count).by(-1) # 削除ボタンをクリックするとPostが1つ減る
# リダイレクト後の画面確認
expect(current_path).to eq('/')
expect(page).to have_content('投稿が削除されました') # フラッシュメッセージを表示
expect(page).not_to have_link("/posts/#{@post.id}") # 削除した投稿(の詳細ページへのリンク)が存在しない
end
end
context '投稿したユーザーでログインしていない場合' do
it '削除ボタンを表示しない' do
visit "/posts/#{@post.id}"
expect(page).not_to have_button('削除')
end
it '直接リクエストを投げても削除されない' do
visit "/posts/#{@post.id}"
expect do
delete post_path(@post) # 投稿データを削除するリクエストを送る
end.not_to change(Post, :count)
end
end
end
describe 'ナビゲーションバーの検証' do
context 'ログインしていない場合' do
before { visit '/' }
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/')
end
it 'ログ投稿リンクを表示しない' do
expect(page).not_to have_link('ログ投稿', href: '/posts/new')
end
end
context 'ログインしている場合' do
before do
user = create(:user) # ログイン用のユーザーを作成
sign_in user # 作成したユーザーでログイン
visit '/'
end
it 'ログ一覧リンクを表示する' do
expect(page).to have_link('ログ一覧', href: '/')
end
it 'ログ投稿リンクを表示する' do
expect(page).to have_link('ログ投稿', href: '/posts/new')
end
end
end
end