Como verificar uma resposta JSON usando o RSpec?

145

Eu tenho o seguinte código no meu controlador:

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

No meu teste do controlador RSpec, quero verificar se um determinado cenário recebe uma resposta json bem-sucedida, portanto, tive a seguinte linha:

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

Embora ao executar meus testes, recebo o seguinte erro:

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

Estou verificando a resposta incorretamente?

Efervescer
fonte

Respostas:

164

Você pode examinar o objeto de resposta e verificar se ele contém o valor esperado:

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

EDITAR

Alterar isso para a posttorna um pouco mais complicado. Aqui está uma maneira de lidar com isso:

 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

Observe que mock_modelnão responderá to_json, portanto, stub_modelé necessária uma instância de modelo real.

zetético
fonte
1
Eu tentei isso e, infelizmente, ele diz que recebeu uma resposta de "". Isso pode ser um erro no controlador?
Fizz
Além disso, a ação é 'criar', importa o uso de uma postagem em vez de um get?
Fizz
Sim, você desejaria post :createcom um hash de parâmetros válido.
Zetetic #
4
Você também deve especificar o formato solicitado. post :create, :format => :json
Robert Speicher
8
JSON é apenas uma string, uma sequência de caracteres e sua ordem é importante. {"a":"1","b":"2"}e {"b":"2","a":"1"}não são cadeias iguais que notam objetos iguais. Você não deve comparar seqüências de caracteres, mas objetos JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}.
skalee
165

Você pode analisar o corpo da resposta assim:

parsed_body = JSON.parse(response.body)

Então você pode fazer suas afirmações contra esse conteúdo analisado.

parsed_body["foo"].should == "bar"
brentmc79
fonte
6
isso parece muito mais fácil. Obrigado.
Tbcs
Primeiro, muito obrigado. Uma pequena correção: JSON.parse (response.body) retorna uma matriz. ['foo'] no entanto, procura por uma chave em um valor de hash. O corrigido é analisado_body [0] ['foo'].
CanCeylan
5
JSON.parse retorna apenas uma matriz se houver uma matriz na cadeia JSON.
redjohn
2
@PriyankaK, se estiver retornando HTML, sua resposta não será json. Verifique se sua solicitação está especificando o formato json.
precisa saber é o seguinte
10
Você também pode usar b = JSON.parse(response.body, symoblize_names: true)para poder acessá-los usando símbolos da seguinte maneira:b[:foo]
FloatingRock
45

Partindo da resposta de Kevin Trowbridge

response.header['Content-Type'].should include 'application/json'
lightyrs
fonte
21
O rspec-rails fornece um comparador para isso: expect (response.content_type) .to eq ("application / json") #
1155 Dan Garland
4
Você não poderia simplesmente usar em Mime::JSONvez de 'application/json'?
FloatingRock
@FloatingRock I acha que vai precisarMime::JSON.to_s
Edgar Ortega
34

Há também a json_spec gem, que vale a pena dar uma olhada

https://github.com/collectiveidea/json_spec

acw
fonte
Esta biblioteca também inclui definições de etapas de pepino que parecem bastante úteis.
22812 Kevin Bedell
13

Simples e fácil de fazer isso.

# 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
Chitrank Samaiya
fonte
11

Você também pode definir uma função auxiliar dentro spec/support/

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

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

e use json_bodysempre que precisar acessar a resposta JSON.

Por exemplo, dentro das especificações de sua solicitação, você pode usá-lo diretamente

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
Lorem Ipsum Dolor
fonte
8

Outra abordagem para testar apenas uma resposta JSON (não que o conteúdo contenha um valor esperado) é analisar a resposta usando o ActiveSupport:

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

Se a resposta não for analisável JSON, uma exceção será lançada e o teste falhará.

Clinton
fonte
7

Você pode olhar no 'Content-Type'cabeçalho para ver se está correto?

response.header['Content-Type'].should include 'text/javascript'
Kevin Trowbridge
fonte
1
Pois render :json => object, acredito que o Rails retorna um cabeçalho Content-Type de 'application / json'.
lightyrs
1
Melhor opção que eu penso:response.header['Content-Type'].should match /json/
bricker 18/07/12
Gosto porque mantém as coisas simples e não adiciona uma nova dependência.
Webpapaya
5

Ao usar o Rails 5 (atualmente ainda em beta), há um novo método, parsed_bodyna resposta do teste, que retornará a resposta analisada como a última solicitação codificada.

O commit no GitHub: https://github.com/rails/rails/commit/eee3534b

Koen.
fonte
O Rails 5 saiu da versão beta, junto com #parsed_body. Ainda não está documentado, mas pelo menos o formato JSON funciona. Observe que as teclas ainda são strings (em vez de símbolos); portanto, pode-se achar útil #deep_symbolize_keysou #with_indifferent_accessútil (eu gosto do último).
Franklin Yu
1

Se você quiser tirar proveito das diferenças de hash fornecidas pelo Rspec, é melhor analisar o corpo e comparar com um hash. Maneira mais simples que eu encontrei:

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

  expect(JSON.parse(response.body)).to eql(expected_body)
end
Damien Roche
fonte
1

Solução de comparação JSON

Rende um Diff limpo, mas potencialmente grande:

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

Exemplo de saída do console a partir de dados reais:

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

(Obrigado a comentar por @floatingrock)

Solução de comparação de cadeias

Se você deseja uma solução revestida de ferro, evite usar analisadores que possam introduzir igualdade de falsos positivos; compare o corpo da resposta com uma sequência. por exemplo:

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

Mas essa segunda solução é menos visualmente amigável, pois usa JSON serializado, o que incluiria muitas aspas vazias.

Solução de correspondência personalizada

Costumo escrever para mim mesmo um correspondente personalizado que faz um trabalho muito melhor de identificar exatamente em qual slot recursivo os caminhos JSON diferem. Adicione o seguinte às suas 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

Exemplo de uso 1:

expect_response(response, :no_content)

Exemplo de uso 2:

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

Exemplo de saída:

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

Outro exemplo de saída para demonstrar uma incompatibilidade profunda em uma matriz aninhada:

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

Como você pode ver, a saída informa EXATAMENTE onde corrigir o JSON esperado.

Amin Ariana
fonte
0

Encontrei um correspondência de cliente aqui: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Coloque-o em spec / support / matchers / have_content_type.rb e certifique-se de carregar coisas do suporte com algo parecido com isto em spec / spec_helper.rb

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

Aqui está o código em si, para o caso de ele desaparecer do link fornecido.

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
Zeke Fast
fonte
0

Muitas das respostas acima estão um pouco desatualizadas, portanto, este é um resumo rápido de uma versão mais recente do RSpec (3.8+). Esta solução não gera avisos do rubocop-rspec e está alinhada com as práticas recomendadas do rspec :

Uma resposta JSON bem-sucedida é identificada por duas coisas:

  1. O tipo de conteúdo da resposta é application/json
  2. O corpo da resposta pode ser analisado sem erros

Supondo que o objeto de resposta seja o assunto anônimo do teste, ambas as condições acima podem ser validadas usando os correspondentes incorporados do 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 você estiver preparado para nomear seu assunto, os testes acima podem ser mais simplificados:

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
UrsaDK
fonte