Come verificare la risposta JSON usando RSpec?


145

Ho il seguente codice nel mio controller:

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

Nel mio test del controller RSpec voglio verificare che un determinato scenario riceva una risposta JSON di successo, quindi ho avuto la seguente riga:

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

Sebbene quando eseguo i miei test ottengo il seguente errore:

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

Sto verificando la risposta in modo errato?

Risposte:


164

È possibile esaminare l'oggetto risposta e verificare che contenga il valore previsto:

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

MODIFICARE

Cambiare questo in un postrende un po 'più complicato. Ecco un modo per gestirlo:

 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

Si noti che mock_modelnon risponderà to_json, quindi stub_modelè necessaria una o un'istanza del modello reale.


1
Ho provato questo e purtroppo dice che ha ottenuto una risposta di "". Potrebbe essere un errore nel controller?
Fizz

Anche l'azione è 'crea', importa che io usi un post invece di un get?
Fizz

Sì, vorresti post :createcon un hash di parametri valido.
zetetico

4
Dovresti anche specificare il formato che stai richiedendo. post :create, :format => :json
Robert Speicher,

8
JSON è solo una stringa, una sequenza di caratteri e il loro ordine è importante. {"a":"1","b":"2"}e {"b":"2","a":"1"}non sono stringhe uguali che indicano oggetti uguali. Non devi confrontare stringhe ma oggetti, JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}invece.
Skalee,

165

È possibile analizzare il corpo della risposta in questo modo:

parsed_body = JSON.parse(response.body)

Quindi puoi fare le tue affermazioni contro quel contenuto analizzato.

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

6
questo sembra molto più semplice. Grazie.
martedì

Innanzitutto, grazie mille. Una piccola correzione: JSON.parse (response.body) restituisce un array. ['pippo'] cerca comunque una chiave in un valore di hash. Quello corretto è parsed_body [0] ['foo'].
CanCeylan,

5
JSON.parse restituisce un array solo se c'era un array nella stringa JSON.
Redjohn il

2
@PriyankaK se restituisce HTML, la tua risposta non è json. Assicurati che la tua richiesta specifichi il formato json.
brentmc79,

10
Puoi anche usarli in b = JSON.parse(response.body, symoblize_names: true)modo da poter accedere ad essi usando simboli come questi:b[:foo]
FloatingRock

45

Basandosi sulla risposta di Kevin Trowbridge

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

21
rspec-rails fornisce un matcher per questo: wait (response.content_type) .to eq ("application / json")
Dan Garland

4
Non potresti semplicemente usare Mime::JSONinvece di 'application/json'?
FloatingRock,

@FloatingRock Penso che ti serviràMime::JSON.to_s
Edgar Ortega il


13

Modo semplice e facile per farlo.

# 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

Puoi anche definire una funzione di supporto all'interno spec/support/

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

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

e usare json_body ogni volta che è necessario accedere alla risposta JSON.

Ad esempio, all'interno delle specifiche della richiesta è possibile utilizzarlo direttamente

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

Un altro approccio per testare solo una risposta JSON (non che il contenuto all'interno contenga un valore previsto) è analizzare la risposta utilizzando ActiveSupport:

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

Se la risposta non è analizzabile JSON verrà generata un'eccezione e il test fallirà.


7

Potresti guardare 'Content-Type'nell'intestazione per vedere che è corretto?

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

1
Per render :json => object, credo Rails restituisce un header Content-Type di 'application / json'.
Lightyrs,

1
La migliore opzione penso:response.header['Content-Type'].should match /json/
muratore

Piace perché mantiene le cose semplici e non aggiunge una nuova dipendenza.
webpapaya,

5

Quando si utilizza Rails 5 (attualmente ancora in versione beta), esiste un nuovo metodo, parsed_bodysulla risposta del test, che restituirà la risposta analizzata come l'ultima codifica della richiesta.

L'impegno su GitHub: https://github.com/rails/rails/commit/eee3534b


Rails 5 è uscito dalla beta, insieme a #parsed_body. Non è ancora documentato, ma almeno il formato JSON funziona. Nota che i tasti sono ancora stringhe (anziché simboli), quindi potresti trovare uno #deep_symbolize_keyso #with_indifferent_accessutile (mi piace il secondo).
Franklin Yu,

1

Se vuoi trarre vantaggio dall'hash diff fornito da Rspec, è meglio analizzare il corpo e confrontarlo con un hash. Il modo più semplice che ho trovato:

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

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

1

Soluzione di confronto JSON

Produce un Diff pulito ma potenzialmente grande:

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

Esempio di output della console da dati reali:

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

(Grazie al commento di @floatingrock)

Soluzione di confronto delle stringhe

Se vuoi una soluzione rivestita di ferro, dovresti evitare di usare parser che potrebbero introdurre un'eguaglianza falsa positiva; confrontare il corpo della risposta con una stringa. per esempio:

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

Ma questa seconda soluzione è meno visivamente intuitiva in quanto utilizza JSON serializzato che includerebbe molte virgolette sfuggite.

Soluzione di abbinamento personalizzata

Tendo a scrivere me stesso un matcher personalizzato che fa un lavoro molto migliore di individuare esattamente quale slot ricorsivo differiscono per i percorsi JSON. Aggiungi quanto segue alle tue macro 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

Esempio di utilizzo 1:

expect_response(response, :no_content)

Esempio di utilizzo 2:

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

Esempio di output:

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

Un altro esempio di output per dimostrare una mancata corrispondenza in profondità in un array nidificato:

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

Come puoi vedere, l'output ti dice ESATTAMENTE dove correggere il tuo JSON previsto.


0

Ho trovato un cliente corrispondente qui: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Inseriscilo in spec / support / matchers / have_content_type.rb e assicurati di caricare roba dal supporto con qualcosa del genere in te spec / spec_helper.rb

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

Ecco il codice stesso, nel caso in cui sia scomparso dal link indicato.

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

Molte delle risposte di cui sopra sono un po 'obsolete, quindi questo è un breve riepilogo per una versione più recente di RSpec (3.8+). Questa soluzione non genera avvisi da rubocop-rspec ed è in linea con le migliori pratiche di rspec :

Una risposta JSON riuscita è identificata da due cose:

  1. Il tipo di contenuto della risposta è application/json
  2. Il corpo della risposta può essere analizzato senza errori

Supponendo che l'oggetto response sia l'oggetto anonimo del test, entrambe le condizioni sopra possono essere validate usando i matcher integrati di 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

Se sei pronto a nominare il tuo argomento, i test di cui sopra possono essere ulteriormente semplificati:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.