Posted at

RSpec で subject & expect する時の注意点

More than 3 years have passed since last update.

どの段階でテストコードが評価されているか注意する。

expectis_expected 何が違うの??な人のための記事です。


subject を使うのはどういうとき?

定義元のコメントを見てみると・・


rspec/core/memoized_helpers.rb

# Declares a `subject` for an example group which can then be wrapped

# with `expect` using `is_expected` to make it the target of an
# expectation in a concise, one-line example.
# ...
def subject(name=nil, &block)
if name
let(name, &block)
alias_method :subject, name

self::NamedSubjectPreventSuper.__send__(:define_method, name) do
raise NotImplementedError, "`super` in named subjects is not supported"
end
else
let(:subject, &block)
end
end


is_expected を使って簡潔な一行 example を書くため、らしい。

なのでsubject と is_expected はペアで使う。

subject の実態は要するに let (※letがわからない場合はここが詳しい解説)

そして let! ではないので、この時点ではまだ評価されてない。


is_expected は何なのか?


spec/controllers/books_controller_spec.rb

# Wraps the `subject` in `expect` to make it the target of an expectation.

# ...
# Designed to read nicely for one-liners.
def is_expected
expect(subject)
end

インスタンス変数の subject を呼び出してる。

なのでこいつをペアで呼んであげると、subjectに渡したブロックが評価される。


特にハマリポイントはない王道パターンの使い方


spec/controllers/books_controller_spec.rb

RSpec.describe BooksController do

describe "GET 'index'" do
subject(:action) { get 'index' }
it { is_expected.to be_success } # OK
end
end



expect を使いたい時は?

is_expected じゃなくて expect だと何が違う?


spec/controllers/books_controller_spec.rb


# さっきのテストコードの続き

let(:param) { {title: 'My Book'} } # 適当にcreateが成立するパラメータが定義されてるとする

# ステータスコードチェックしてみる
describe "POST 'create'" do
# json のリクエストみたいに成功したら 201 (:craeted)が戻るコントローラーを例に
subject(:action) { post 'create', params }
it { expect(subject.status).to eq 201 } # 1.OK
it { expect(action.status).to eq 201 } # 2.OK
it { expect(response.status).to eq 201 } # 3.NG
end



それぞれ何をやってるか


1. expect(subject.status).to eq 201

subject -> let(:subject)で定義されたブロック(上の例で { post 'create' }のとこ)を評価して、 subject.status をチェック


2. expect(action.status).to eq 201

subject の name として action が渡ってるので、 ここでは subject == action

1と同じ。


3. expect(response.status).to eq 201

subject を評価していないので、 インスタンス変数 response の値は初期値のまま変わってない。

のでエラー

(status==200が初期値なので、GET#index の所で 200 チェックしてると、この現象に気づきづらい)


通したい場合


expect(subject) で評価

 describe "POST 'create'" do

subject(:action) { post 'create', params }
it do
expect(subject) # ここで評価
expect(response.status).to eq 201
end
end


subject だけでも良い

 describe "POST 'create'" do

subject(:action) { post 'create', params }
it do
subject # ここで評価
expect(response.status).to eq 201
end
end


subject!

let, let!の関係から推測・・

 describe "POST 'create'" do

subject!(:action) { post 'create', params }
it do
expect(response.status).to eq 201
end
end

でもOK :)



statusチェックしたいだけなら is_expected でもできる

  it { is_expected.to have_http_status(201) }        # OK

it { is_expected.to have_attributes(status: 201) } # OK

・・のでこんなとこでハマる人居ないはず;;


assigns チェックの時はこんな感じ

  let!(:books) { FactoryGirl.create_list(:book, 10) }

subject(:action) { get 'index' }
it do
expect(subject)
expect(assigns(:books)).to eq(books)
end

  let!(:books) { FactoryGirl.create_list(:book, 10) }

subject(:action) { get 'index' }
it do
subject
expect(assigns(:books)).to eq(books)
end

  let!(:books) { FactoryGirl.create_list(:book, 10) }

subject!(:action) { get 'index' }
it do
expect(assigns(:books)).to eq(books)
end

上の「通したい場合」のどのパターンでも良い。