RSpec でリクエストを実行するときは let が便利

before ブロック

RSpec でコントローラや API のテストを作るとき、リクエストの実行を before ブロックに記述することがある。

describe FooController do
  before { post :create }
  
  context 'with some data' do
    before { create_data }
    it { expect(response).to have_http_status(:ok) }
  end
end

create_data は、リクエストに必要なデータを作る前処理で、リクエスト実行前に走ることを想定している。

しかし、このコードは想定通りの動きにならない。実際には、外側の before ブロックでリクエストが実行された後、内側のブロックで create_data が実行される。これは、RSpec の仕様で、before ブロック は describe や context ブロックの外側から実行されるためだ。

relishapp.com

before ブロックでリクエストを実行する方法は、気軽でよく使ってしまいがちだが、このようにリクエストの前に何らかの前処理を差し込みたい場合に不便である。

let ヘルパー

そこで、以下のようなリクエストを実行する let を定義する。fetch_response を呼び出すと、リクエストが走り、レスポンスオブジェクトを返す*1

# controller spec の `response` ヘルパーと重複するので、それっぽい別の名前をつけた
let(:fetched_response) { post :create }

このようにリクエストの実行を let で定義しておけば、好きなタイミングでリクエストを実行できるようになるので、リクエストの前に前処理を差し込むが簡単になる。

describe FooController do  
  context 'with some data' do
    before { create_data }
    it { expect(fetched_response).to have_http_status(:ok) }
  end
end

好きなタイミングでリクエストを実行できるので、change マッチャーも簡単に使える。

it { expect { fetched_response }.to change(Post, :count).by(1) }

JSON API のテストならこんなふうに書くのが便利。JSON.parsesymbolize_names: true を渡せば、parse した Hash のキーをシンボルに自動で変換してくれる。

describe FooController do
  let(:fetched_response) { post :create }  
  let(:parsed_response) do
    JSON.parse(fetched_response.body, symbolize_names: true)
  end

  context 'with invalid request' do
    before { create_data }

    it do
      expect(parsed_response).to match(
        type: 'validation_failed',
        errors: [
          { field: 'xxx', code: 'xxxx' }
        ]
      )
    end
  end
end

こんな感じに、1 example に複数の expect を書いても、実行されるリクエストはただ一回。そう、let ならね。

describe FooController do  
  let(:fetched_response) { post :create }  

  context 'with invalid request' do
    before { create_data }

    it do
      expect(fetched_response).to have_http_status(:ok)
      expect(fetched_response.body).to eq({ status: 'ok' }.to_json) 
    end
  end
end

*1:controller spec の get や post のようなリクエストを実行するヘルパーは、レスポンスオブジェクトをそのまま返すため