Ruby on Rails

【Ruby on Rails5.2】RSpecの実行順序について実際に動かしてみた

・RSpec時のlet・beforeの実行順序について
・let!の存在と実行順序について

業務が忙しくなったおかげで、少し間が経ちましたがRailsの学習は継続中です。

今回はE2Eテストで利用しているRSpecについて教材だけでは理解が難しい箇所を整理したいと思います。

Railsの学習はDocker環境下で行っていますので合わせて参考いただければ幸いです。

RSpecの書き方と疑問

教材には下記のようなテストコードがありました。

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
            # 作成者がユーザーAであるタスクを作成する
            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
                # ユーザーAが作成したタスクの名前が画面上に表示されていないことを確認
                expect(page).to have_no_content '最初のタスク'
            end
        end
    end
end

ユーザーA、ユーザーBでログインしたときの表示テストのテストコードです。
「describe>describe」の中にある「let」でテストデータ(ユーザーA,B)作成を定義しています。
2つの「context」内でユーザーA、ユーザーBを作成するようにしています。

そして「before」の中には、ユーザーAのみが確認できるトランザクションデータが作成しています。
そこまでは良いのですが、その後のログイン処理が腑に落ちませんでした。

beforeと謳っているので、contextより前に実行されるのでは無いのか。そうなるとlet前にログイン処理が走るのでエラーになると思いました。

このテストコードは正解になるので私の考えは間違っているのですが、何故beforeより前にcontext(let)が動くのかを整理したいと思います。

実行順序

結論から言いますと、beforeはitの前に実行されます。

ブロックレベル的にbeforeはcontextの前に実行されると思いましたがそうではなくitの前に実行されます。
上記で言うところのデータ確認前(it)にユーザーが作成(let)され、ログイン処理(before)が行われます。

  1. describe
  2. context(この中にletがあればそれを呼ぶ)
  3. before
  4. it

ただし、describeの中に複数のcontextがあれば、先に全てのcontextが評価されます
実際に動かしてみたコードと結果は下記となります。

require 'rails_helper'

describe '実行順序1', type: :system do
    describe '実行順序2' do
        before do
            puts 'before'
        end
        context 'ユーザーAがログインしているとき' do
            puts 'context 1'
            it 'ユーザーAが作成したタスクが表示される' do
                puts 'it A'
            end
        end

        context 'ユーザーBがログインしているとき' do
            puts 'context 2'
            it 'ユーザーAが作成したタスクが表示されない1' do
                puts 'it B 1'
            end
            it 'ユーザーAが作成したタスクが表示されない2' do
                puts 'it B 2'
            end
        end
    end
end
# bundle exec rspec ./spec/system/test_spec.rb 
context 1
context 2
before
it A
.before
it B 1
.before
it B 2
.

Finished in 0.39048 seconds (files took 8.13 seconds to load)
3 examples, 0 failures

上記のように2つのcontextが先に動きその後に、it(before)が行われます。

let! の存在

letについては上記でもある通り、定義してそれを明示的に呼ばない限り評価されない存在です。
letは呼ばれて初めて価値が生まれます。

一方、let!は明示的に呼ばなくてもbefore前に自動で呼ばれる存在です。
複数のdescribeの中で共通して利用する処理がある場合は、let!を使った方が呼ぶ手間が省けます。

  1. describe
  2. context(この中にletがあればそれを呼ぶ)
  3. let!
  4. before
  5. it
require 'rails_helper'

describe '実行順序1', type: :system do
    describe '実行順序2' do
        let!(:test){ puts 'let! 1' }
        before do
            puts 'before'
        end
        context 'ユーザーAがログインしているとき' do
            puts 'context 1'
            it 'ユーザーAが作成したタスクが表示される' do
                puts 'it A'
            end
        end

        context 'ユーザーBがログインしているとき' do
            puts 'context 2'
            it 'ユーザーAが作成したタスクが表示されない1' do
                puts 'it B 1'
            end
            it 'ユーザーAが作成したタスクが表示されない2' do
                puts 'it B 2'
            end
        end
    end
end
# bundle exec rspec ./spec/system/test_spec.rb 
context 1
context 2
let! 1
before
it A
.let! 1
before
it B 1
.let! 1
before
it B 2
.

Finished in 0.34572 seconds (files took 7.77 seconds to load)
3 examples, 0 failures

上記のように、5行目にlet!を定義しただけでbefore前に毎回let!が呼ばれているのが分かります。

参考記事

Rspecの処理順について
https://glodia.jp/blog/4230/