はじめに
今回もRailsの自動テストについて学んでいきます。
前回に引き続きタスク管理アプリにシステムテストを実装していきます。
処理の共通化
beforeを使った共通化
前回作成した一覧表示機能のSpecに「ユーザーAがログインしているとき」「ユーザーBがログインしているとき」という2つのcontextを記述しました。
この2つのcontextのbeforeは以下のようになっています。
spec/system/tasks_spec.rb
before do
visit login_path
fill_in 'メールアドレス', with: 'a@example.com'
fill_in 'パスワード', with: 'password'
click_button 'ログインする'
end
before do
FactoryBot.create(:user, name: 'ユーザーB', email: 'b@example.com')
visit login_path
fill_in 'メールアドレス', with: 'b@example.com'
fill_in 'パスワード', with: 'password'
click_button 'ログインする'
end
どちらのbeforeでもログイン処理を行なっています。入力するメールアドレスに違いはあるものの、コード自体の共通点は多いです。この類似したコードを共通化します。
Specでは同じ階層のdescribe/context内で共通する処理については、そのdescribe/contextの1つ上の階層のbefore内に処理を記述することで共通化できます。
まずは「ユーザーAがログインしているとき」というcontextで実行していたログイン処理を上の階層のbefore内に移動させて、「ユーザーBがログインしているとき」というcontextのbefore内のログイン処理を取り除きます。
spec/system/tasks_spec.rb
require 'rails_helper'
describe 'タスク管理機能', type: :system do
describe '一覧表示機能' do
before do
user_a = FactoryBot.create(:user, name: 'ユーザーA', email: 'a@example.com')
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
visit login_path
fill_in 'メールアドレス', with: 'a@example.com'
fill_in 'パスワード', with: 'password'
click_button 'ログインする'
end
context 'ユーザーAがログインしている時' do
it 'ユーザーAが作成したタスクが表示される' do
expect(page).to have_content '最初のタスク'
end
end
context 'ユーザーBがログインしているとき' do
before do
FactoryBot.create(:user, name: 'ユーザーB', email: 'b@example.com')
end
it 'ユーザーAが作成したタスクが表示されない' do
expect(page).to have_no_content '最初のタスク'
end
end
end
end
このままではユーザーBのテストに関してもユーザーAでログインしてしまうため不完全です。
この問題を解決するために、共通のログイン処理中の「誰が」ログインするかという部分を「空欄」のようにして、この「空欄」を埋めるコードだけをユーザーA側とユーザーB側それぞれに配置します。
先に処理される外側に共通処理を記述し、内側の複数のdescribe/contextごとに細かな違いを定義するために「let」を使用します。
letを使った共通化
以下のletを使った記法で、変数のように使えるオブジェクトを定義できます。
let(定義名) { 定義の内容 }
コードのイメージは以下の通りです。login_userというletをSpecのあるdescribe/context野中で定義する場合、その外側でも内側でも、login_userを変数のように呼び出して使用できます。
describ '・・・' do
before do
# ここでlogin_userを使える
end
context '・・・' do
let(:login_user) { FactoryBot.create(:user) }
it '・・・' do
# ここでlogin_userを使える
end
context '・・・' do
before do
# ここでlogin_userを使える
end
it '・・・' do
# ここでlogin_userを使える
end
end
end
end
上記を参考にspec/system/tasks_spec.rbを以下のように修正します。
spec/system/tasks_spec.rb
require 'rails_helper'
describe 'タスク管理機能', type: :system do
describe '一覧表示機能' do
let(:user_a) { FactoryBot.create(:user, name: 'ユーザーA', email: 'a@example.com') } // ①
let(:user_b) { FactoryBot.create(:user, name: 'ユーザーB', email: 'b@example.com') } // ①
before do
FactoryBot.create(:task, name: '最初のタスク', user: user_a) // ②
visit login_path
fill_in 'メールアドレス', with: login_user.email // ③
fill_in 'パスワード', with: login_user.password
click_button 'ログインする'
end
context 'ユーザーAがログインしている時' do
let(:login_user) { user_a } // ④
it 'ユーザーAが作成したタスクが表示される' do
expect(page).to have_content '最初のタスク'
end
end
context 'ユーザーBがログインしているとき' do
let(:login_user) { user_b } // ⑤
it 'ユーザーAが作成したタスクが表示されない' do
expect(page).to have_no_content '最初のタスク'
end
end
end
end
上記のコードの内容は以下のようになっています。
- ①…ユーザーAとユーザーBをletで定義しています。
- ②…①で定義したuser_aを利用してタスクを作成します。
- ③…ここから続く行は共通化するログイン処理です。メールアドレスやパスワードを、内側のcontextないで定義されているlogin_userから取得しています。これにより誰がログインするかに関わらず同じコードを使えます。
- ④⑤…③で使うlogin_userをletで定義しています。
それではテストを実行してみます。
$ bundle exec rspec spec/system/tasks_spec.rb
..
Finished in 11.32 seconds (files took 7.05 seconds to load)
2 examples, 0 failures
無事成功しました。
以上で処理の共通化ができました。
letについて
先ほど紹介したletは呼び出されたタイミングで初めて実行されます。
そのためlogin_userというletを定義したとしても、login_userを一度も呼び出さずにログインすると、ユーザーが作られずログインに失敗します。
describe '一覧表示機能' do
let(:login_user) { FactoryBot.create(:user, name: 'ユーザーA', email: 'a@example.com') }
before do
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
visit login_path
fill_in 'メールアドレス', with: 'a@example.com'
fill_in 'パスワード', with: 'password'
click_button 'ログインする' #=>失敗
end
・・・
end
このような場合letの代わりにlet!を利用するとbeforeの前にユーザーが登録され、ログインできるようになります。let!は呼び出される場合と呼び出されない場合があるがデータは常に作りたい時などに便利に利用できます。
describe '一覧表示機能' do
let!(:login_user) { FactoryBot.create(:user, name: 'ユーザーA', email: 'a@example.com') }
before do
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
visit login_path
fill_in 'メールアドレス', with: 'a@example.com'
fill_in 'パスワード', with: 'password'
click_button 'ログインする' #=>成功
end
・・・
end
タスク詳細表示機能のSystem Spec
タスク詳細表示機能のSpecを作ります。
spec/system/tasks_spec.rbを以下の通りに修正します。
spec/system/tasks_spec.rb
require 'rails_helper'
describe 'タスク管理機能', type: :system do
let(:user_a) { FactoryBot.create(:user, name: 'ユーザーA', email: 'a@example.com') }
let(:user_b) { FactoryBot.create(:user, name: 'ユーザーB', email: 'b@example.com') }
let(:task_a) { FactoryBot.create(:task, name: '最初のタスク', user: user_a) }
before do
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
visit login_path
fill_in 'メールアドレス', with: login_user.email
fill_in 'パスワード', with: login_user.password
click_button 'ログインする'
end
describe '一覧表示機能' do
context 'ユーザーAがログインしている時' do
let(:login_user) { user_a }
it 'ユーザーAが作成したタスクが表示される' do
expect(page).to have_content '最初のタスク'
end
end
context 'ユーザーBがログインしているとき' do
let(:login_user) { user_b }
it 'ユーザーAが作成したタスクが表示されない' do
expect(page).to have_no_content '最初のタスク'
end
end
end
describe '詳細表示機能' do
context 'ユーザーAがログインしている時' do
let(:login_user) { user_a }
before do
visit task_path(task_a)
end
it 'ユーザーAが作成したタスクが表示される' do
expect(page).to have_content '最初のタスク'
end
end
end
end
詳細表示機能もユーザーのログインを前提としているため、一覧表示機能のdescribe内にあったログイン処理のbeforeを上の階層に移動しました。
さらにユーザーAのタスク登録にはlet!を利用しています。
テストを実行してみます。
$ bundle exec rspec spec/system/tasks_spec.rb
...
Finished in 4.52 seconds (files took 2.7 seconds to load)
3 examples, 0 failures
無事成功しました。
終わりに
今回はSpec内の処理の共通化をした後、前回に続きSystem Specを追加しました。
今回はここまでとなります。
Railsの自動テストについては次回で最後になります。
コメント