RSpecを使用してJSONレスポンスを確認する方法は?

私のコントローラーには以下のコードがあります:

format.json { render :json => { 
        :flashcard  => @flashcard,
        :lesson     => @lesson,
        :success    => true
} 

RSpecのコントローラテストでは、あるシナリオが成功のjsonレスポンスを受け取ることを確認したいので、次のような行を用意しました:

controller.should_receive(:render).with(hash_including(:success => true))

しかし、テストを実行すると、次のようなエラーが発生します:

Failure/Error: controller.should_receive(:render).with(hash_including(:success => false))
 (#<AnnoController:0x00000002de0560>).render(hash_including(:success=>false))
     expected: 1 time
     received: 0 times

レスポンスの確認が間違っているのでしょうか?

ソリューション

レスポンスオブジェクトを調べ、期待値が含まれていることを確認することができます:

@expected = { 
        :flashcard  => @flashcard,
        :lesson     => @lesson,
        :success    => true
}.to_json
get :action # replace with action name / params as necessary
response.body.should == @expected

EDIT

これを post に変えると、少し厄介なことになります。以下はその処理方法です:

 it "responds with JSON" do
    my_model = stub_model(MyModel,:save=>true)
    MyModel.stub(:new).with({'these' => 'params'}) { my_model }
    post :create, :my_model => {'these' => 'params'}, :format => :json
    response.body.should == my_model.to_json
  end

なお、mock_modelto_json に反応しないので、stub_model か本物のモデルインスタンスが必要です。

解説 (7)

このようにレスポンスボディを解析することができます:

parsed_body = JSON.parse(response.body)

そして、その解析された内容に対して、アサーションを行うことができるのです。

parsed_body["foo"].should == "bar"
解説 (7)

ケビントローブリッジの答えの構築。

response.header['Content-Type'].should include 'application/json'
解説 (3)

また、json_spec gemもあり、これは一見の価値があります。

https://github.com/collectiveidea/json_spec

解説 (1)

これを行うためのシンプルで簡単な方法。

# set some variable on success like :success => true in your controller
controller.rb
render :json => {:success => true, :data => data} # on success

spec_controller.rb
parse_json = JSON(response.body)
parse_json["success"].should == true
解説 (1)

spec / support /内でヘルパー関数を定義することもできます。

module ApiHelpers
  def json_body
    JSON.parse(response.body)
  end
end

RSpec.configure do |config| 
  config.include ApiHelpers, type: :request
end

JSON応答にアクセスする必要がある場合は常に「json_body」を使用します。

たとえば、リクエストスペックの内部では、直接使用できます。

context 'when the request contains an authentication header' do
  it 'should return the user info' do
    user  = create(:user)
    get URL, headers: authenticated_header(user)

    expect(response).to have_http_status(:ok)
    expect(response.content_type).to eq('application/vnd.api+json')
    expect(json_body["data"]["attributes"]["email"]).to eq(user.email)
    expect(json_body["data"]["attributes"]["name"]).to eq(user.name)
  end
end
解説 (0)

JSON応答のみをテストするもう1つのアプローチ(内部のコンテンツに期待値が含まれているわけではありません)は、ActiveSupportを使用して応答を解析することです。

ActiveSupport::JSON.decode(response.body).should_not be_nil

応答が解析可能なJSONでない場合、例外がスローされ、テストは失敗します。

解説 (0)

「コンテンツタイプ」ヘッダーを調べて、それが正しいことを確認できます?

response.header['Content-Type'].should include 'text/javascript'
解説 (3)

Rails 5(現在はベータ版)を使用する場合、テスト応答に「parsed_body」という新しいメソッドがあり、最後のリクエストがエンコードされたものとして解析された応答を返します。

GitHubのコミット:https://github.com/rails/rails/commit/eee3534b

解説 (1)

Rspecが提供するハッシュdiffを利用したい場合は、体を解析してハッシュと比較することをお勧めします。 私が見つけた最も簡単な方法:

it 'asserts json body' do
  expected_body = {
    my: 'json',
    hash: 'ok'
  }.stringify_keys

  expect(JSON.parse(response.body)).to eql(expected_body)
end
解説 (0)

ここでカスタマーマッチャーを見つけました:https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

spec / support / matchers / have_content_type.rbに入れ、spec / spec_helper.rbでこのようなものをサポートからロードするようにしてください。

Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f}

指定されたリンクから消えた場合に備えて、コード自体を次に示します。

RSpec::Matchers.define :have_content_type do |content_type|
  CONTENT_HEADER_MATCHER = /^(.*?)(?:; charset=(.*))?$/

  chain :with_charset do |charset|
    @charset = charset
  end

  match do |response|
    _, content, charset = *content_type_header.match(CONTENT_HEADER_MATCHER).to_a

    if @charset
      @charset == charset && content == content_type
    else
      content == content_type
    end
  end

  failure_message_for_should do |response|
    if @charset
      "Content type #{content_type_header.inspect} should match #{content_type.inspect} with charset #{@charset}"
    else
      "Content type #{content_type_header.inspect} should match #{content_type.inspect}"
    end
  end

  failure_message_for_should_not do |model|
    if @charset
      "Content type #{content_type_header.inspect} should not match #{content_type.inspect} with charset #{@charset}"
    else
      "Content type #{content_type_header.inspect} should not match #{content_type.inspect}"
    end
  end

  def content_type_header
    response.headers['Content-Type']
  end
end
解説 (0)