RSpec

Rspecのbeforeとletの挙動を検証

More than 1 year has passed since last update.

rspecの挙動が分かりづらかったので簡単にまとめてみました。

Gemfile.lock

    rspec (3.6.0)
      rspec-core (~> 3.6.0)
      rspec-expectations (~> 3.6.0)
      rspec-mocks (~> 3.6.0)
    rspec-core (3.6.0)
      rspec-support (~> 3.6.0)
    rspec-expectations (3.6.0)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.6.0)
    rspec-mocks (3.6.0)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.6.0)
    rspec-rails (3.6.0)
      actionpack (>= 3.0)
      activesupport (>= 3.0)
      railties (>= 3.0)
      rspec-core (~> 3.6.0)
      rspec-expectations (~> 3.6.0)
      rspec-mocks (~> 3.6.0)
      rspec-support (~> 3.6.0)
    rspec-support (3.6.0)

検証コード

create(:model)factorygirlで何かしらのApplicationRecordのオブジェクトを作る想定

require 'rails_helper'

RSpec.describe do
  let(:model) do
    m = create(:model)
    p 'outer let:' + m.id.to_s
    m
  end
  before do
    m = create(:model)
    p 'outer before:' + m.id.to_s
  end
  before(:each) do
    m = create(:model)
    p 'outer before(each):' + m.id.to_s
  end
  before(:all) do
    m = create(:model)
    p 'outer before(all):' + m.id.to_s
  end

  context do
    let(:model) do
      m = create(:model)
      p 'inner let:' + m.id.to_s
      m
    end
    before do
      m = create(:model)
      p 'inner before:' + m.id.to_s
    end
    before(:each) do
      m = create(:model)
      p 'inner before(each):' + m.id.to_s
    end
    before(:all) do
      m = create(:model)
      p 'inner before(all):' + m.id.to_s
    end
    subject do
      m = model
      p 'subject1:' + m.id.to_s
      m
    end

    example { expect(subject.id).to eq(model.id) }
    it do
      m = model
      p 'it:' + m.id.to_s
      expect(subject.id).to eq(m.id)
    end
  end

  context do
    subject do
      m = model
      p 'subject2:' + m.id.to_s
      m
    end

    example { expect(subject.id).to eq(model.id) }
  end
end

とした場合、アウトプットは下記の通りになる

"outer before(all):1"
"inner before(all):2"
"outer before:3"
"outer before(each):4"
"inner before:5"
"inner before(each):6"
"inner let:7"
"subject1:7"
"outer before:8"
"outer before(each):9"
"inner before:10"
"inner before(each):11"
"inner let:12"
"it:12"
"subject1:12"
"outer before:13"
"outer before(each):14"
"outer let:15"
"subject2:15"

まとめ

  • テストの実行単位は it or exampleであり、基本的に実行単位ごとに変数は初期化される(以下、このテスト実行単位をテストプロセスと表記します)
  • beforeメソッドはブロックのスコープ内で有効
  • before(:all)はブロック内で一度実行される(ブロック内にテストプロセスが複数あっても実行は一度のみ)
  • beforebefore(:each)は同じ。ブロック内のテストプロセスごとに実行される
  • subjectlet(:subject)の略。テスト対象を表す変数。expect(subject).tois_expected.toと略せる。
  • letについて
    • 遅延評価変数。ブロック内でコールされたタイミングでletブロック内の処理を行い結果をletの引き数の変数に格納
    • 同じブロック内で複数回コールされる場合、初回にletブロック内の処理を行いその結果のオブジェクトを、2回目以降は初回の実行時に作成されたオブジェクトを返却する
    • ブロックのスコープ内とスコープ外の2箇所で同じ名前のlet変数が定義されていた場合、ブロック内のletだけが評価される

おまけ(letとlet!)

require 'rails_helper'

RSpec.describe do
  let(:model) do
    m = create(:model)
    p 'outer let:' + m.id.to_s
    m
  end

  context do
    let!(:dummy_model) do
      m = create(:model)
      p 'dummy let!:' + m.id.to_s
      m
    end
    subject do
      m = model
      p 'subject:' + m.id.to_s
      m
    end

    example { expect(subject.id).to eq(model.id) }
    it do
      m = model
      p 'it:' + m.id.to_s
      expect(subject.id).to eq(m.id)
    end
  end
end
"dummy let!:1"
"outer let:2"
"subject:2"
"dummy let!:3"
"outer let:4"
"it:4"
"subject:4"
  • let!は変数がコールされなくてもテストプロセスのはじめに実行される(beforeの前か後かはコードの記述順で決まる)