Comment vérifier une réponse JSON à l'aide de RSpec?


145

J'ai le code suivant dans mon contrôleur:

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

Dans mon test de contrôleur RSpec, je veux vérifier qu'un certain scénario reçoit une réponse json de succès, j'ai donc eu la ligne suivante:

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

Bien que lorsque j'exécute mes tests, j'obtiens l'erreur suivante:

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

Est-ce que je vérifie la réponse de manière incorrecte?

Réponses:


164

Vous pouvez examiner l'objet de réponse et vérifier qu'il contient la valeur attendue:

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

ÉDITER

Changer cela en a postrend les choses un peu plus délicates. Voici un moyen de gérer cela:

 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

Notez que mock_modelcela ne répondra pas to_json, donc stub_modelune instance de modèle réelle ou une instance réelle est nécessaire.


1
J'ai essayé ceci et malheureusement il dit qu'il a obtenu une réponse de "". Cela pourrait-il être une erreur dans le contrôleur?
Fizz

De plus, l'action est «créer», est-ce important que j'utilise un message au lieu d'un get?
Fizz

Oui, vous voudriez post :createavec un hachage de paramètres valides.
zetetic

4
Vous devez également spécifier le format que vous demandez. post :create, :format => :json
Robert Speicher

8
JSON n'est qu'une chaîne, une séquence de caractères et leur ordre est important. {"a":"1","b":"2"}et {"b":"2","a":"1"}ne sont pas des chaînes égales qui notent des objets égaux. Vous ne devez pas comparer des chaînes mais des objets, faites JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}plutôt.
skalee

165

Vous pouvez analyser le corps de la réponse comme ceci:

parsed_body = JSON.parse(response.body)

Ensuite, vous pouvez faire vos affirmations contre ce contenu analysé.

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

6
cela semble beaucoup plus facile. Merci.
tbaums

Tout d'abord, merci beaucoup. Une petite correction: JSON.parse (response.body) renvoie un tableau. ['foo'] recherche cependant une clé dans une valeur de hachage. Celui corrigé est parsed_body [0] ['foo'].
CanCeylan

5
JSON.parse ne renvoie un tableau que s'il y avait un tableau dans la chaîne JSON.
redjohn

2
@PriyankaK s'il renvoie du HTML, votre réponse n'est pas json. Assurez-vous que votre demande spécifie le format json.
brentmc79

10
Vous pouvez également utiliser b = JSON.parse(response.body, symoblize_names: true)pour pouvoir y accéder en utilisant des symboles comme celui-ci:b[:foo]
FloatingRock

45

S'appuyant sur la réponse de Kevin Trowbridge

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

21
rspec-rails fournit un matcher pour ceci: expect (response.content_type) .to eq ("application / json")
Dan Garland

4
Ne pourriez-vous pas simplement utiliser à la Mime::JSONplace de 'application/json'?
FloatingRock

@FloatingRock je pense que vous aurez besoinMime::JSON.to_s
Edgar Ortega


13

Un moyen simple et facile de le faire.

# 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

Vous pouvez également définir une fonction d'assistance à l'intérieur spec/support/

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

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

et utilise json_body chaque fois que vous avez besoin d'accéder à la réponse JSON.

Par exemple, dans votre spécification de demande, vous pouvez l'utiliser directement

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

Une autre approche pour tester uniquement une réponse JSON (pas que le contenu contienne une valeur attendue), consiste à analyser la réponse à l'aide d'ActiveSupport:

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

Si la réponse n'est pas analysable JSON, une exception sera levée et le test échouera.


7

Vous pourriez regarder dans l'en- 'Content-Type'tête pour voir que c'est correct?

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

1
Pour render :json => object, je crois que Rails renvoie un en-tête Content-Type de 'application / json'.
lightyrs

1
Meilleure option je pense:response.header['Content-Type'].should match /json/
briqueteur

Aimez-le parce qu'il simplifie les choses et n'ajoute pas de nouvelle dépendance.
webpapaya

5

Lors de l'utilisation de Rails 5 (actuellement encore en version bêta), il existe une nouvelle méthode, parsed_bodysur la réponse de test, qui retournera la réponse analysée comme à quoi la dernière requête a été encodée.

Le commit sur GitHub: https://github.com/rails/rails/commit/eee3534b


Rails 5 est sorti de la version bêta, avec #parsed_body. Ce n'est pas encore documenté, mais au moins le format JSON fonctionne. Notez que les clés sont toujours des chaînes (au lieu de symboles), donc on peut trouver l'une #deep_symbolize_keysou l' autre #with_indifferent_accessutile (j'aime ce dernier).
Franklin Yu

1

Si vous souhaitez profiter du hash diff fourni par Rspec, il est préférable d'analyser le corps et de le comparer à un hachage. Le moyen le plus simple que j'ai trouvé:

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

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

1

Solution de comparaison JSON

Donne un Diff propre mais potentiellement important:

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

Exemple de sortie de console à partir de données réelles:

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

(Merci au commentaire de @floatingrock)

Solution de comparaison de chaînes

Si vous voulez une solution à toute épreuve, vous devez éviter d'utiliser des analyseurs qui pourraient introduire une fausse égalité positive; comparez le corps de la réponse à une chaîne. par exemple:

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

Mais cette deuxième solution est moins conviviale visuellement car elle utilise du JSON sérialisé qui inclurait beaucoup de guillemets échappés.

Solution de correspondance personnalisée

J'ai tendance à écrire moi-même un matcher personnalisé qui fait un bien meilleur travail pour identifier exactement à quel emplacement récursif les chemins JSON diffèrent. Ajoutez ce qui suit à vos macros 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

Exemple d'utilisation 1:

expect_response(response, :no_content)

Exemple d'utilisation 2:

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

Exemple de sortie:

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

Un autre exemple de sortie pour démontrer une incompatibilité profonde dans un tableau imbriqué:

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

Comme vous pouvez le voir, la sortie vous indique EXACTEMENT où corriger votre JSON attendu.


0

J'ai trouvé un correspondant client ici: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Mettez-le dans spec / support / matchers / have_content_type.rb et assurez-vous de charger des éléments du support avec quelque chose comme ça dans votre spec / spec_helper.rb

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

Voici le code lui-même, juste au cas où il aurait disparu du lien donné.

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

Un grand nombre des réponses ci-dessus sont un peu dépassées, il s'agit donc d'un résumé rapide d'une version plus récente de RSpec (3.8+). Cette solution ne déclenche aucun avertissement de la part de rubocop-rspec et est conforme aux meilleures pratiques de rspec :

Une réponse JSON réussie est identifiée par deux choses:

  1. Le type de contenu de la réponse est application/json
  2. Le corps de la réponse peut être analysé sans erreur

En supposant que l'objet de réponse est le sujet anonyme du test, les deux conditions ci-dessus peuvent être validées à l'aide des correspondants intégrés de 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

Si vous êtes prêt à nommer votre sujet, les tests ci-dessus peuvent être simplifiés davantage:

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.