RSpecを使用してJSON応答を確認する方法


145

コントローラに次のコードがあります。

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

応答を間違ってチェックしていますか?

回答:


164

応答オブジェクトを調べて、期待される値が含まれていることを確認できます。

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

編集

これをaに変更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_modelには応答しませんto_jsonので、どちらか、stub_modelまたは実際のモデルインスタンスが必要とされています。


1
試してみたところ、残念ながら「」の応答があったようです。これはコントローラーのエラーですか?
Fizz

また、アクションは「作成」ですが、取得の代わりに投稿を使用するよりも重要ですか?
Fizz

はい、post :create有効なパラメーターハッシュを使用します。
ゼーテティック

4
また、要求する形式を指定する必要があります。post :create, :format => :json
Robert Speicher

8
JSONは単なる文字列であり、文字のシーケンスとその順序が重要です。 {"a":"1","b":"2"}そして、{"b":"2","a":"1"}同等のオブジェクトを記譜同じ文字列ではありません。文字列ではJSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}なくオブジェクトを比較するべきではなく、代わりに行います。
skalee

165

次のようにしてレスポンスボディを解析できます:

parsed_body = JSON.parse(response.body)

次に、その解析されたコンテンツに対してアサーションを作成できます。

parsed_body["foo"].should == "bar"

6
これはずっと簡単なようです。ありがとう。
tbaums

まず、どうもありがとうございました。小さな修正:JSON.parse(response.body)は配列を返します。['foo']ただし、ハッシュ値でキーを検索します。修正されたのはparsed_body [0] ['foo']です。
CanCeylan

5
JSON.parseは、JSON文字列に配列がある場合にのみ配列を返します。
redjohn

2
@PriyankaKがHTMLを返す場合、応答はjsonではありません。リクエストがjson形式を指定していることを確認してください。
brentmc79 2013

10
あなたはまた、使用することができb = JSON.parse(response.body, symoblize_names: true)、あなたがそうのような記号を使用してそれらにアクセスできるように:b[:foo]
FloatingRock



13

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

# 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

11

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

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

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

そして使う json_body、JSON応答にアクセスする必要があるときはいつでもします。

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

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

8

JSON応答のみをテストする別の方法(内部のコンテンツに期待値が含まれていないこと)では、ActiveSupportを使用して応答を解析します。

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

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


7

'Content-Type'ヘッダーを調べて、それが正しいことを確認できますか?

response.header['Content-Type'].should include 'text/javascript'

1
についてはrender :json => object、Railsは 'application / json'のContent-Typeヘッダーを返すと思います。
lightyrs 2012

1
私が思う最高のオプション:response.header['Content-Type'].should match /json/
ブリック

それは物事をシンプルに保ち、新しい依存関係を追加しないのでそれが好きです。
webpapaya

5

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

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


Rails 5はとともに、ベータ版から抜け出しました#parsed_body。まだ文書化されていませんが、少なくともJSON形式は機能します。キーは(記号ではなく)依然として文字列であるため、どちらか#deep_symbolize_keysまたはどちらかが#with_indifferent_access役立つ場合があります(私は後者が好きです)。
フランクリンユー

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

1

JSON比較ソリューション

クリーンですが潜在的に大きなDiffを生成します:

actual = JSON.parse(response.body, symbolize_names: true)
expected = { foo: "bar" }
expect(actual).to eq expected

実際のデータからのコンソール出力の例:

expected: {:story=>{:id=>1, :name=>"The Shire"}}
     got: {:story=>{:id=>1, :name=>"The Shire", :description=>nil, :body=>nil, :number=>1}}

   (compared using ==)

   Diff:
   @@ -1,2 +1,2 @@
   -:story => {:id=>1, :name=>"The Shire"},
   +:story => {:id=>1, :name=>"The Shire", :description=>nil, ...}

(@floatingrockによるコメントに感謝)

文字列比較ソリューション

アイアンクラッドのソリューションが必要な場合は、偽陽性の等価性を導入する可能性のあるパーサーの使用を避けてください。レスポンスボディを文字列と比較します。例えば:

actual = response.body
expected = ({ foo: "bar" }).to_json
expect(actual).to eq expected

ただし、この2番目のソリューションは、エスケープされた引用符を多く含むシリアル化されたJSONを使用するため、視覚的にあまりわかりません。

カスタムマッチャーソリューション

私は自分でカスタムマッチャーを作成する傾向があります。これは、JSONパスが異なる再帰的スロットを正確に特定するはるかに優れた仕事をします。以下をrspecマクロに追加します。

def expect_response(actual, expected_status, expected_body = nil)
  expect(response).to have_http_status(expected_status)
  if expected_body
    body = JSON.parse(actual.body, symbolize_names: true)
    expect_json_eq(body, expected_body)
  end
end

def expect_json_eq(actual, expected, path = "")
  expect(actual.class).to eq(expected.class), "Type mismatch at path: #{path}"
  if expected.class == Hash
    expect(actual.keys).to match_array(expected.keys), "Keys mismatch at path: #{path}"
    expected.keys.each do |key|
      expect_json_eq(actual[key], expected[key], "#{path}/:#{key}")
    end
  elsif expected.class == Array
    expected.each_with_index do |e, index|
      expect_json_eq(actual[index], expected[index], "#{path}[#{index}]")
    end
  else
    expect(actual).to eq(expected), "Type #{expected.class} expected #{expected.inspect} but got #{actual.inspect} at path: #{path}"
  end
end

使用例1:

expect_response(response, :no_content)

使用例2:

expect_response(response, :ok, {
  story: {
    id: 1,
    name: "Shire Burning",
    revisions: [ ... ],
  }
})

出力例:

Type String expected "Shire Burning" but got "Shire Burnin" at path: /:story/:name

ネストされた配列の深い不一致を示す別の出力例:

Type Integer expected 2 but got 1 at path: /:story/:revisions[0]/:version

ご覧のとおり、出力は、予想されるJSONを修正する場所を正確に示しています。


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

上記の回答の多くは少し古くなっているので、これはRSpecの最新バージョン(3.8以降)の簡単な要約です。このソリューションはrubocop-rspecから警告を出さず、rspecのベストプラクティス一致しています

成功したJSON応答は、次の2つによって識別されます。

  1. 応答のコンテンツタイプは application/json
  2. 応答の本文はエラーなしで解析できます

応答オブジェクトがテストの匿名の対象であると仮定すると、上記の条件は両方とも、Rspecの組み込みマッチャーを使用して検証できます。

context 'when response is received' do
  subject { response }

  # check for a successful JSON response
  it { is_expected.to have_attributes(content_type: include('application/json')) }
  it { is_expected.to have_attributes(body: satisfy { |v| JSON.parse(v) }) }

  # validates OP's condition
  it { is_expected.to satisfy { |v| JSON.parse(v.body).key?('success') }
  it { is_expected.to satisfy { |v| JSON.parse(v.body)['success'] == true }
end

主題に名前を付ける準備ができている場合は、上記のテストをさらに簡略化できます。

context 'when response is received' do
  subject(:response) { response }

  it 'responds with a valid content type' do
    expect(response.content_type).to include('application/json')
  end

  it 'responds with a valid json object' do
    expect { JSON.parse(response.body) }.not_to raise_error
  end

  it 'validates OPs condition' do
    expect(JSON.parse(response.body, symoblize_names: true))
      .to include(success: true)
  end
end
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.