Bagaimana cara memeriksa respons JSON menggunakan RSpec?


145

Saya memiliki kode berikut di controller saya:

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

Dalam tes pengontrol RSpec saya, saya ingin memverifikasi bahwa skenario tertentu tidak menerima respons json sukses jadi saya memiliki baris berikut:

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

Meskipun ketika saya menjalankan tes saya, saya mendapatkan kesalahan berikut:

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

Apakah saya salah mengecek respons?

Jawaban:


164

Anda dapat memeriksa objek respons dan memverifikasi bahwa itu berisi nilai yang diharapkan:

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

EDIT

Mengubah ini postmenjadi agak rumit. Berikut cara untuk menanganinya:

 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

Catatan yang mock_modeltidak akan merespons to_json, sehingga salah satu stub_modelatau contoh model nyata diperlukan.


1
Saya mencoba ini dan sayangnya katanya mendapat respons "". Mungkinkah ini kesalahan pada controller?
Fizz

Juga aksinya adalah 'buat', apakah lebih penting daripada saya menggunakan postingan daripada mendapatkan?
Fizz

Ya, Anda ingin post :createdengan hash parameter yang valid.
zetetic

4
Anda juga harus menentukan format yang Anda minta. post :create, :format => :json
Robert Speicher

8
JSON hanyalah string, urutan karakter dan urutannya yang penting. {"a":"1","b":"2"}dan {"b":"2","a":"1"}bukan string yang sama yang mencatat objek yang sama. Anda tidak harus membandingkan string tetapi objek, lakukan JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}saja.
skalee

165

Anda dapat menguraikan badan respons seperti ini:

parsed_body = JSON.parse(response.body)

Kemudian Anda dapat membuat pernyataan terhadap konten yang diurai tersebut.

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

6
ini tampaknya jauh lebih mudah. Terima kasih.
tbaums

Pertama, terima kasih banyak. Koreksi kecil: JSON.parse (response.body) mengembalikan array. ['foo'] namun mencari kunci dalam nilai hash. Yang dikoreksi adalah parsed_body [0] ['foo'].
CanCeylan

5
JSON.parse hanya mengembalikan array jika ada array di string JSON.
redjohn

2
@ PrayankaK jika mengembalikan HTML, maka respons Anda bukan json. Pastikan permintaan Anda menentukan format json.
brentmc79

10
Anda juga dapat menggunakan b = JSON.parse(response.body, symoblize_names: true)sehingga Anda dapat mengaksesnya menggunakan simbol-simbol seperti:b[:foo]
FloatingRock

45

Membangun dari jawaban Kevin Trowbridge

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

21
rspec-rails menyediakan pencocokan untuk ini: expect (response.content_type) .to eq ("application / json")
Dan Garland

4
Tidak bisakah Anda menggunakan Mime::JSONsaja 'application/json'?
FloatingRock

@ FloatingRock Saya pikir Anda akan perluMime::JSON.to_s
Edgar Ortega


13

Sederhana dan mudah untuk melakukannya.

# 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

Anda juga dapat mendefinisikan fungsi pembantu di dalam spec/support/

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

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

dan gunakan json_bodykapan pun Anda perlu mengakses respons JSON.

Misalnya, di dalam spesifikasi permintaan Anda, Anda dapat menggunakannya secara langsung

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

Pendekatan lain untuk menguji hanya untuk respons JSON (bukan berarti konten di dalamnya mengandung nilai yang diharapkan), adalah untuk menguraikan respons menggunakan ActiveSupport:

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

Jika responsnya tidak parsable, JSON pengecualian akan dilemparkan dan tes akan gagal.


7

Anda bisa melihat ke 'Content-Type'header untuk melihat apakah itu benar?

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

1
Karena render :json => object, saya percaya Rails mengembalikan header Tipe-Konten dari 'application / json'.
lightyrs

1
Pilihan terbaik saya pikir:response.header['Content-Type'].should match /json/
tukang bata

Menyukainya karena itu membuat hal-hal sederhana dan tidak menambah ketergantungan baru.
webpapaya

5

Saat menggunakan Rails 5 (saat ini masih dalam versi beta), ada metode baru, parsed_bodypada respons tes, yang akan mengembalikan respons yang diuraikan seperti apa permintaan terakhir dikodekan.

Komit di GitHub: https://github.com/rails/rails/commit/eee3534b


Rails 5 berhasil keluar dari beta, bersama dengan #parsed_body. Belum didokumentasikan, tetapi setidaknya format JSON berfungsi. Perhatikan bahwa kunci masih berupa string (bukan simbol), jadi orang mungkin menemukan salah satu #deep_symbolize_keysatau #with_indifferent_accessberguna (saya suka yang terakhir).
Franklin Yu

1

Jika Anda ingin memanfaatkan hash diff yang disediakan oleh Rspec, lebih baik mengurai tubuh dan membandingkannya dengan hash. Cara paling sederhana yang saya temukan:

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

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

1

Solusi perbandingan JSON

Menghasilkan Diff yang bersih namun berpotensi besar:

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

Contoh output konsol dari data nyata:

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

(Terima kasih untuk komentar oleh @floatingrock)

Solusi perbandingan string

Jika Anda menginginkan solusi berbalut besi, Anda harus menghindari penggunaan parser yang dapat memperkenalkan kesetaraan positif palsu; bandingkan tubuh respons terhadap string. misalnya:

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

Tetapi solusi kedua ini kurang ramah secara visual karena menggunakan JSON berseri yang akan mencakup banyak tanda kutip yang lolos.

Solusi pencocokan khusus

Saya cenderung menulis sendiri pencocokan kustom yang melakukan pekerjaan yang jauh lebih baik untuk menentukan dengan tepat di mana slot rekursif jalur JSON berbeda. Tambahkan berikut ini ke makro rspec Anda:

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

Contoh penggunaan 1:

expect_response(response, :no_content)

Contoh penggunaan 2:

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

Contoh output:

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

Contoh output lain untuk menunjukkan ketidakcocokan jauh di dalam array bersarang:

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

Seperti yang Anda lihat, output memberitahu Anda PERSIS di mana untuk memperbaiki JSON yang Anda harapkan.


0

Saya menemukan pencocokan pelanggan di sini: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Letakkan di spec / support / matchers / have_content_type.rb dan pastikan untuk memuat hal-hal dari dukungan dengan sesuatu seperti ini di dalam Anda spec / spec_helper.rb

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

Ini kode itu sendiri, kalau-kalau kode itu hilang dari tautan yang diberikan.

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

Banyak jawaban di atas agak ketinggalan zaman, jadi ini adalah ringkasan cepat untuk versi RSpec yang lebih baru (3,8+). Solusi ini tidak menimbulkan peringatan dari rubocop-rspec dan sejalan dengan praktik terbaik rspec :

Respons JSON yang berhasil diidentifikasi oleh dua hal:

  1. Jenis konten dari tanggapan adalah application/json
  2. Badan respons dapat diuraikan tanpa kesalahan

Dengan asumsi bahwa objek respons adalah subjek uji anonim, kedua kondisi di atas dapat divalidasi menggunakan pencocokan bawaan 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

Jika Anda siap menyebutkan nama subjek, maka tes di atas dapat disederhanakan lebih lanjut:

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
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.