Wie kann ich mit RSpec nach einer JSON-Antwort suchen?


145

Ich habe den folgenden Code in meinem Controller:

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

In meinem RSpec-Controller-Test möchte ich überprüfen, ob ein bestimmtes Szenario eine erfolgreiche JSON-Antwort erhält, sodass ich die folgende Zeile hatte:

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

Obwohl beim Ausführen meiner Tests die folgende Fehlermeldung angezeigt wird:

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

Überprüfe ich die Antwort falsch?

Antworten:


164

Sie können das Antwortobjekt untersuchen und sicherstellen, dass es den erwarteten Wert enthält:

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

BEARBEITEN

Wenn Sie dies in a poständern, wird es etwas schwieriger. Hier ist eine Möglichkeit, damit umzugehen:

 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

Beachten Sie, dass dies mock_modelnicht reagiert to_json, sodass entweder eine stub_modeloder eine echte Modellinstanz benötigt wird.


1
Ich habe es versucht und leider heißt es, dass es eine Antwort von "" bekam. Könnte dies ein Fehler in der Steuerung sein?
Fizz

Außerdem lautet die Aktion "Erstellen". Ist es wichtig, dass ich einen Beitrag anstelle eines Abrufs verwende?
Fizz

Ja, Sie möchten post :createmit einem gültigen Parameter-Hash.
Zetetic

4
Sie sollten auch das gewünschte Format angeben. post :create, :format => :json
Robert Speicher

8
JSON ist nur eine Zeichenfolge, eine Folge von Zeichen und deren Reihenfolge. {"a":"1","b":"2"}und {"b":"2","a":"1"}sind nicht gleiche Zeichenfolgen, die gleiche Objekte notieren. Sie sollten keine Zeichenfolgen, sondern Objekte vergleichen, sondern dies tun JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}.
Skalee

165

Sie können den Antworttext folgendermaßen analysieren:

parsed_body = JSON.parse(response.body)

Dann können Sie Ihre Aussagen gegen diesen analysierten Inhalt machen.

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

6
das scheint viel einfacher zu sein. Vielen Dank.
Baums

Zunächst vielen Dank. Eine kleine Korrektur: JSON.parse (response.body) gibt ein Array zurück. ['foo'] sucht jedoch nach einem Schlüssel in einem Hashwert. Der korrigierte ist parsed_body [0] ['foo'].
CanCeylan

5
JSON.parse gibt nur dann ein Array zurück, wenn die JSON-Zeichenfolge ein Array enthält.
Redjohn

2
@PriyankaK Wenn HTML zurückgegeben wird, lautet Ihre Antwort nicht json. Stellen Sie sicher, dass Ihre Anfrage das JSON-Format angibt.
Brentmc79

10
Sie können auch verwenden, b = JSON.parse(response.body, symoblize_names: true)damit Sie mit folgenden Symbolen darauf zugreifen können:b[:foo]
FloatingRock

45

Aufbauend auf Kevin Trowbridges Antwort

response.header['Content-Type'].should include 'application/json'

21
rspec-Rails bietet einen Matcher dafür: erwarten (response.content_type) .to eq ("application / json")
Dan Garland

4
Könnten Sie nicht einfach Mime::JSONstatt verwenden 'application/json'?
FloatingRock

@FloatingRock Ich denke, Sie werden brauchenMime::JSON.to_s
Edgar Ortega


13

Einfach und leicht zu tun, um dies zu tun.

# 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

Sie können auch eine Hilfsfunktion definieren spec/support/

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

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

und verwenden json_body Sie diese Option, wenn Sie auf die JSON-Antwort zugreifen müssen.

In Ihrer Anforderungsspezifikation können Sie sie beispielsweise direkt verwenden

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

Ein anderer Ansatz, um nur auf eine JSON-Antwort zu testen (nicht, dass der Inhalt einen erwarteten Wert enthält), besteht darin, die Antwort mit ActiveSupport zu analysieren:

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

Wenn die Antwort nicht analysierbar ist, wird eine Ausnahme ausgelöst und der Test schlägt fehl.


7

Sie könnten in die 'Content-Type'Kopfzeile schauen, um zu sehen, dass es richtig ist?

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

1
Denn render :json => object, wie ich glaube Rails ein Content-Type - Header 'application / json' zurückgibt.
Lightyrs

1
Beste Option, denke ich:response.header['Content-Type'].should match /json/
Maurer

Mag es, weil es die Dinge einfach hält und keine neue Abhängigkeit hinzufügt.
Webpapaya

5

Bei Verwendung von Rails 5 (derzeit noch in der Beta) gibt es eine neue Methode: parsed_body für die Testantwort, die die Antwort zurückgibt, die als die analysiert wurde, bei der die letzte Anforderung codiert wurde.

Das Commit auf GitHub: https://github.com/rails/rails/commit/eee3534b


Rails 5 hat es aus der Beta geschafft, zusammen mit #parsed_body. Es ist noch nicht dokumentiert, aber zumindest das JSON-Format funktioniert. Beachten Sie, dass die Tasten immer noch Zeichenfolgen sind (anstelle von Symbolen), so dass man entweder #deep_symbolize_keysoder #with_indifferent_accessnützlich finden kann (ich mag letzteres).
Franklin Yu

1

Wenn Sie das von Rspec bereitgestellte Hash-Diff nutzen möchten, ist es besser, den Körper zu analysieren und mit einem Hash zu vergleichen. Der einfachste Weg, den ich gefunden habe:

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-Vergleichslösung

Ergibt einen sauberen, aber möglicherweise großen Diff:

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

Beispiel für die Konsolenausgabe aus realen Daten:

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, ...}

(Dank Kommentar von @floatingrock)

String-Vergleichslösung

Wenn Sie eine eiserne Lösung wünschen, sollten Sie die Verwendung von Parsern vermeiden, die zu einer falsch positiven Gleichheit führen können. Vergleichen Sie den Antworttext mit einer Zeichenfolge. z.B:

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

Diese zweite Lösung ist jedoch weniger visuell benutzerfreundlich, da sie serialisiertes JSON verwendet, das viele maskierte Anführungszeichen enthält.

Benutzerdefinierte Matcher-Lösung

Ich neige dazu, mir einen benutzerdefinierten Matcher zu schreiben, der viel besser genau bestimmt, an welchem ​​rekursiven Slot sich die JSON-Pfade unterscheiden. Fügen Sie Ihren rspec-Makros Folgendes hinzu:

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

Anwendungsbeispiel 1:

expect_response(response, :no_content)

Anwendungsbeispiel 2:

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

Beispielausgabe:

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

Eine weitere Beispielausgabe zur Demonstration einer Nichtübereinstimmung tief in einem verschachtelten Array:

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

Wie Sie sehen können, sagt Ihnen die Ausgabe GENAU, wo Sie Ihren erwarteten JSON reparieren müssen.


0

Ich habe hier einen Kunden-Matcher gefunden: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Fügen Sie es in spec / support / matchers / have_content_type.rb ein und stellen Sie sicher, dass Sie Inhalte aus dem Support mit so etwas in Ihre spec / spec_helper.rb laden

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

Hier ist der Code selbst, nur für den Fall, dass er aus dem angegebenen Link verschwunden ist.

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

Viele der oben genannten Antworten sind etwas veraltet, daher ist dies eine kurze Zusammenfassung für eine neuere Version von RSpec (3.8+). Diese Lösung gibt keine Warnungen von rubocop-rspec aus und entspricht den Best Practices von rspec :

Eine erfolgreiche JSON-Antwort wird durch zwei Dinge identifiziert:

  1. Der Inhaltstyp der Antwort ist application/json
  2. Der Hauptteil der Antwort kann fehlerfrei analysiert werden

Unter der Annahme, dass das Antwortobjekt das anonyme Subjekt des Tests ist, können beide oben genannten Bedingungen mithilfe der in Rspec integrierten Matcher validiert werden:

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

Wenn Sie bereit sind, Ihr Thema zu benennen, können die obigen Tests weiter vereinfacht werden:

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.