¿Cómo verificar una respuesta JSON usando RSpec?

145

Tengo el siguiente código en mi controlador:

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

En mi prueba de controlador RSpec, quiero verificar que un determinado escenario reciba una respuesta json exitosa, así que obtuve la siguiente línea:

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

Aunque cuando ejecuto mis pruebas me sale el siguiente error:

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

¿Estoy verificando la respuesta incorrectamente?

Efervescencia
fuente

Respuestas:

164

Puede examinar el objeto de respuesta y verificar que contiene el 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

Cambiar esto a a lo posthace un poco más complicado. Aquí hay una manera de manejarlo:

 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

Tenga en cuenta que mock_modelno responderá to_json, por lo que stub_modelse necesita una instancia de modelo real.

zetetic
fuente
1
Intenté esto y desafortunadamente dice que obtuvo una respuesta de "". ¿Podría ser esto un error en el controlador?
Fizz
Además, la acción es 'crear', ¿importa que use una publicación en lugar de un get?
Fizz
Sí, querrás post :createcon un hash de parámetros válido.
Zetetic
44
También debe especificar el formato que está solicitando. post :create, :format => :json
Robert Speicher
8
JSON es solo una cadena, una secuencia de caracteres y su orden importa. {"a":"1","b":"2"}y {"b":"2","a":"1"}no son cadenas iguales que anotan objetos iguales. No debes comparar cadenas sino objetos, hazlo en su JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}lugar.
skalee
165

Puede analizar el cuerpo de respuesta de esta manera:

parsed_body = JSON.parse(response.body)

Luego puede hacer sus afirmaciones contra ese contenido analizado.

parsed_body["foo"].should == "bar"
brentmc79
fuente
66
Esto parece mucho más fácil. Gracias.
tbaums
Primero muchas gracias. Una pequeña corrección: JSON.parse (response.body) devuelve una matriz. ['foo'] sin embargo, busca una clave en un valor hash. El corregido es parsed_body [0] ['foo'].
CanCeylan
55
JSON.parse solo devuelve una matriz si había una matriz en la cadena JSON.
redjohn
2
@PriyankaK si está devolviendo HTML, entonces su respuesta no es json. Asegúrese de que su solicitud esté especificando el formato json.
brentmc79
10
También puede usar b = JSON.parse(response.body, symoblize_names: true)para poder acceder a ellos usando símbolos como este:b[:foo]
FloatingRock
45

Partiendo de la respuesta de Kevin Trowbridge

response.header['Content-Type'].should include 'application/json'
lightyrs
fuente
21
rspec-rails proporciona una coincidencia para esto: esperar (response.content_type) .to eq ("application / json")
Dan Garland
44
¿No podrías simplemente usar en Mime::JSONlugar de 'application/json'?
FloatingRock
@FloatingRock, creo que necesitarásMime::JSON.to_s
Edgar Ortega
13

Simple y fácil de hacer esto.

# 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
fuente
11

También puede definir una función 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

y úselo json_bodycuando necesite acceder a la respuesta JSON.

Por ejemplo, dentro de la especificación de su solicitud, puede usarla directamente

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
fuente
8

Otro enfoque para probar solo una respuesta JSON (no es que el contenido contenga un valor esperado), es analizar la respuesta usando ActiveSupport:

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

Si la respuesta no es JSON analizable, se lanzará una excepción y la prueba fallará.

Clinton
fuente
7

¿Podrías mirar dentro del 'Content-Type'encabezado para ver si es correcto?

response.header['Content-Type'].should include 'text/javascript'
Kevin Trowbridge
fuente
1
Porque render :json => objectcreo que Rails devuelve un encabezado Content-Type de 'application / json'.
lightyrs
1
La mejor opción creo:response.header['Content-Type'].should match /json/
bricker
Me gusta porque mantiene las cosas simples y no agrega una nueva dependencia.
webpapaya
5

Al usar Rails 5 (actualmente todavía en beta), hay un nuevo método, parsed_bodyen la respuesta de prueba, que devolverá la respuesta analizada en la que se codificó la última solicitud.

El commit en GitHub: https://github.com/rails/rails/commit/eee3534b

Koen
fuente
Rails 5 salió de beta, junto con #parsed_body. Todavía no está documentado, pero al menos el formato JSON funciona. Tenga en cuenta que las teclas siguen siendo cadenas (en lugar de símbolos), por lo que uno puede encontrarlas #deep_symbolize_keyso #with_indifferent_accessútiles (me gusta la última).
Franklin Yu
1

Si desea aprovechar el hash diff que proporciona Rspec, es mejor analizar el cuerpo y compararlo con un hash. La forma más simple que he encontrado:

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
fuente
1

Solución de comparación JSON

Produce un Diff limpio pero potencialmente grande:

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

Ejemplo de salida de consola de datos reales:

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

(Gracias a comentar por @floatingrock)

Solución de comparación de cadenas

Si desea una solución revestida de hierro, debe evitar el uso de analizadores que podrían introducir una falsa igualdad positiva; compare el cuerpo de respuesta con una cadena. p.ej:

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

Pero esta segunda solución es menos amigable visualmente ya que utiliza JSON serializado que incluiría muchas comillas escapadas.

Solución de igualación personalizada

Tiendo a escribirme un matizador personalizado que hace un trabajo mucho mejor al señalar exactamente en qué ranura recursiva difieren las rutas JSON. Agregue lo siguiente a sus 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

Ejemplo de uso 1:

expect_response(response, :no_content)

Ejemplo de uso 2:

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

Salida de ejemplo:

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

Otro resultado de ejemplo para demostrar una falta de coincidencia profunda en una matriz anidada:

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

Como puede ver, la salida le dice EXACTAMENTE dónde arreglar su JSON esperado.

Amin Ariana
fuente
0

Encontré un contacto con el cliente aquí: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Póngalo en spec / support / matchers / have_content_type.rb y asegúrese de cargar cosas del soporte con algo como esto en su spec / spec_helper.rb

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

Aquí está el código en sí mismo, en caso de que desaparezca del enlace dado.

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
fuente
0

Muchas de las respuestas anteriores están un poco desactualizadas, por lo que este es un resumen rápido de una versión más reciente de RSpec (3.8+). Esta solución no genera advertencias de rubocop-rspec y está en línea con las mejores prácticas de rspec :

Una respuesta JSON exitosa se identifica por dos cosas:

  1. El tipo de contenido de la respuesta es application/json
  2. El cuerpo de la respuesta se puede analizar sin errores.

Suponiendo que el objeto de respuesta es el sujeto anónimo de la prueba, ambas condiciones anteriores se pueden validar utilizando los comparadores integrados 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 está preparado para nombrar a su sujeto, las pruebas anteriores se pueden simplificar aún más:

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
fuente