Jak sprawdzić odpowiedź JSON za pomocą RSpec?

145

Mam następujący kod w kontrolerze:

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

W moim teście kontrolera RSpec chcę sprawdzić, czy określony scenariusz otrzyma odpowiedź json sukcesu, więc otrzymałem następujący wiersz:

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

Chociaż po uruchomieniu testów pojawia się następujący błąd:

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

Czy sprawdzam odpowiedź nieprawidłowo?

Syczeć
źródło

Odpowiedzi:

164

Możesz sprawdzić obiekt odpowiedzi i sprawdzić, czy zawiera on oczekiwaną wartość:

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

EDYTOWAĆ

Zmiana tego na a postsprawia, że ​​jest to nieco trudniejsze. Oto sposób na to:

 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

Zauważ, że mock_modelnie zareaguje to_json, więc stub_modelpotrzebna jest albo prawdziwa instancja modelu.

zetetic
źródło
1
Próbowałem tego i niestety jest napisane, że otrzymałem odpowiedź „”. Czy to może być błąd w kontrolerze?
Fizz
Również akcja to „stwórz”, czy to ma znaczenie, że używam posta zamiast get?
Fizz
Tak, chcesz post :createmieć prawidłowy skrót parametrów.
zetetic
4
Powinieneś także określić żądany format. post :create, :format => :json
Robert Speicher
8
JSON to tylko ciąg, sekwencja znaków i ich kolejność ma znaczenie. {"a":"1","b":"2"}i {"b":"2","a":"1"}nie są równymi łańcuchami, które nie oznaczają równych obiektów. Nie powinieneś porównywać łańcuchów, ale obiekty, JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}zamiast tego zrób to .
skalee
165

Możesz przeanalizować treść odpowiedzi w ten sposób:

parsed_body = JSON.parse(response.body)

Następnie możesz sformułować swoje twierdzenia przeciwko przeanalizowanej treści.

parsed_body["foo"].should == "bar"
brentmc79
źródło
6
wydaje się to o wiele łatwiejsze. Dzięki.
tbaums
Po pierwsze, wielkie dzięki. Mała poprawka: JSON.parse (response.body) zwraca tablicę. Jednak ['foo'] szuka klucza w wartości skrótu. Poprawiony to parsed_body [0] ['foo'].
CanCeylan
5
JSON.parse zwraca tablicę tylko wtedy, gdy istnieje tablica w ciągu JSON.
redjohn
2
@PriyankaK, jeśli zwraca HTML, twoja odpowiedź nie jest json. Upewnij się, że żądanie określa format json.
brentmc79
10
Możesz również użyć b = JSON.parse(response.body, symoblize_names: true), aby uzyskać do nich dostęp za pomocą symboli takich jak:b[:foo]
FloatingRock,
45

Opierając się na odpowiedzi Kevina Trowbridge'a

response.header['Content-Type'].should include 'application/json'
lightyrs
źródło
21
rspec-rails zapewnia dopasowanie do tego: Expect (response.content_type) .to eq ("application / json")
Dan Garland
4
Nie mógłbyś po prostu użyć Mime::JSONzamiast 'application/json'?
FloatingRock,
@FloatingRock Myślę, że będziesz potrzebowaćMime::JSON.to_s
Edgar Ortega
34

Jest też klejnot json_spec , który warto zobaczyć

https://github.com/collectiveidea/json_spec

acw
źródło
Ta biblioteka zawiera również definicje kroku Ogórka, które wydają się całkiem przydatne.
Kevin Bedell
13

Prosty i łatwy sposób, aby to zrobić.

# 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
źródło
11

Możesz także zdefiniować funkcję pomocniczą wewnątrz spec/support/

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

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

i używaj, json_bodygdy potrzebujesz uzyskać dostęp do odpowiedzi JSON.

Na przykład w specyfikacji żądania możesz jej użyć bezpośrednio

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
źródło
8

Innym podejściem do testowania tylko dla odpowiedzi JSON (nie mówiącej, że zawarta w niej zawartość zawiera oczekiwaną wartość), jest przeanalizowanie odpowiedzi przy użyciu ActiveSupport:

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

Jeśli odpowiedź nie jest możliwa do przeanalizowania JSON, zostanie zgłoszony wyjątek i test zakończy się niepowodzeniem.

Clinton
źródło
7

Możesz zajrzeć do 'Content-Type'nagłówka, aby zobaczyć, czy jest poprawny?

response.header['Content-Type'].should include 'text/javascript'
Kevin Trowbridge
źródło
1
Dla render :json => objectwierzę, Szyny zwraca nagłówek Content-Type „application / json”.
lightyrs
1
Myślę, że najlepsza opcja:response.header['Content-Type'].should match /json/
bricker
Podoba mi się, ponieważ utrzymuje prostotę i nie dodaje nowej zależności.
webpapaya
5

Podczas korzystania z Rails 5 (obecnie nadal w wersji beta), w parsed_bodyodpowiedzi testowej dostępna jest nowa metoda , która zwróci odpowiedź przeanalizowaną zgodnie z tym, w czym zakodowano ostatnie żądanie.

Zobowiązanie na GitHub: https://github.com/rails/rails/commit/eee3534b

Koen.
źródło
Rails 5 wyszedł z wersji beta wraz z #parsed_body. Nie jest to jeszcze udokumentowane, ale działa przynajmniej format JSON. Należy zauważyć, że klucze są jeszcze łańcuchy (zamiast symboli) tak, jeden może znaleźć zarówno #deep_symbolize_keyslub #with_indifferent_accessprzydatne (I jak ostatniej).
Franklin Yu
1

Jeśli chcesz skorzystać z funkcji hash diff, którą zapewnia Rspec, lepiej przeanalizować treść i porównać ją z hashem. Najprostszy sposób, jaki znalazłem:

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
źródło
1

Rozwiązanie porównawcze JSON

Daje czysty, ale potencjalnie duży Diff:

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

Przykład danych wyjściowych konsoli z rzeczywistych danych:

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

(Dzięki komentarzowi @floatingrock)

Rozwiązanie do porównywania ciągów

Jeśli chcesz uzyskać żelazne rozwiązanie, powinieneś unikać używania parserów, które mogą wprowadzić fałszywie dodatnią równość; porównaj treść odpowiedzi z ciągiem. na przykład:

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

Ale to drugie rozwiązanie jest mniej przyjazne wizualnie, ponieważ wykorzystuje serializowany kod JSON, który zawiera wiele cudzysłowów.

Niestandardowe rozwiązanie dopasowujące

Zwykle piszę sobie niestandardowy element dopasowujący, który znacznie lepiej radzi sobie z dokładnym wskazaniem, w którym dokładnie rekurencyjnym slocie różnią się ścieżki JSON. Dodaj następujące elementy do swoich makr 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

Przykład użycia 1:

expect_response(response, :no_content)

Przykład użycia 2:

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

Przykładowe dane wyjściowe:

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

Inne przykładowe dane wyjściowe pokazujące niezgodność głęboko w zagnieżdżonej tablicy:

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

Jak widać, dane wyjściowe mówią DOKŁADNIE, gdzie naprawić oczekiwany kod JSON.

Amin Ariana
źródło
0

Znalazłem tutaj narzędzie do dopasowywania klientów: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Umieść to w spec / support / matchers / have_content_type.rb i upewnij się, że ładujesz rzeczy ze wsparcia z czymś takim w tobie spec / spec_helper.rb

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

Oto sam kod, na wypadek gdyby zniknął z podanego linku.

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
źródło
0

Wiele z powyższych odpowiedzi jest nieco nieaktualnych, więc jest to krótkie podsumowanie nowszej wersji RSpec (3.8+). To rozwiązanie nie generuje ostrzeżeń ze strony rubocop-rspec i jest zgodne z najlepszymi praktykami rspec :

Pomyślna odpowiedź JSON jest identyfikowana przez dwie rzeczy:

  1. Typ treści odpowiedzi to application/json
  2. Treść odpowiedzi można przeanalizować bez błędów

Zakładając, że obiekt odpowiedzi jest anonimowym przedmiotem testu, oba powyższe warunki można zweryfikować za pomocą wbudowanych dopasowań 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

Jeśli chcesz nazwać swój przedmiot, powyższe testy można jeszcze bardziej uprościć:

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
źródło