ChatGPT解决这个技术问题 Extra ChatGPT

How to check for a JSON response using RSpec?

I have the following code in my controller:

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

In my RSpec controller test I want to verify that a certain scenario does receive a success json response so I had the following line:

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

Although when I run my tests I get the following 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

Am I checking the response incorrectly?


z
zetetic

You can examine the response object and verify that it contains the expected value:

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

EDIT

Changing this to a post makes it a bit trickier. Here's a way to handle it:

 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

Note that mock_model will not respond to to_json, so either stub_model or a real model instance is needed.


I tried this and unfortunately it says that it got a response of " ". Could this be an error in the controller?
Also the action is 'create', does it matter than I use a post instead of a get?
Yes, you'd want post :create with a valid parameters hash.
You should also be specifying the format you're requesting. post :create, :format => :json
JSON is only a string, a sequence of characters and their order matters. {"a":"1","b":"2"} and {"b":"2","a":"1"} are not equal strings which notate equal objects. You should not compare strings but objects, do JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"} instead.
b
brentmc79

You could parse the response body like this:

parsed_body = JSON.parse(response.body)

Then you can make your assertions against that parsed content.

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

this seems a lot easier. Thanks.
First, thanks a lot. A small correction: JSON.parse(response.body) returns an array. ['foo'] however searches for a key in a hash value. The corrected one is parsed_body[0]['foo'].
JSON.parse only returns an array if there was an array in the JSON string.
@PriyankaK if it's returning HTML, then your response is not json. Make sure your request is specifying the json format.
You could also use b = JSON.parse(response.body, symoblize_names: true) so that you can access them using symbols like so: b[:foo]
C
Community

Building off of Kevin Trowbridge's answer

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

rspec-rails provides a matcher for this: expect(response.content_type).to eq("application/json")
Couldn't you just use Mime::JSON instead of 'application/json'?
@FloatingRock I think you will need Mime::JSON.to_s
even better expect(response.content_type).to start_with('application/json') to avoid the charset attribute that may be present.
a
acw

There's also the json_spec gem, which is worth a look

https://github.com/collectiveidea/json_spec


This library also includes Cucumber step definitions that lok pretty useful.
X
X.Creates

You can also define a helper function inside spec/support/

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

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

and use json_body whenever you need to access the JSON response.

For example, inside your request spec you can use it directly

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

z
zishe

Simple and easy to way to do this.

# 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

C
Clinton

Another approach to test just for a JSON response (not that the content within contains an expected value), is to parse the response using ActiveSupport:

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

If the response is not parsable JSON an exception will be thrown and the test will fail.


J
JJD

You could look into the 'Content-Type' header to see that it is correct?

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

For render :json => object, I believe Rails returns a Content-Type header of 'application/json'.
Best option I think: response.header['Content-Type'].should match /json/
Like it because it keeps things simple and doesn't add a new dependency.
K
Koen.

When using Rails 5 (currently still in beta), there's a new method, parsed_body on the test response, which will return the response parsed as what the last request was encoded at.

The commit on GitHub: https://github.com/rails/rails/commit/eee3534b


Rails 5 made it out of beta, along with #parsed_body. It is not yet documented, but at least JSON format works. Note that the keys are still strings (instead of symbols), so one may find either #deep_symbolize_keys or #with_indifferent_access useful (I like the latter).
U
UrsaDK

A lot of the above answers are a bit out of date, so this is a quick summary for a more recent version of RSpec (3.8+). This solution raises no warnings from rubocop-rspec and is inline with rspec best practices:

A successful JSON response is identified by two things:

The content type of the response is application/json The body of the response can be parsed without errors

Assuming that the response object is the anonymous subject of the test, both of the above conditions can be validate using Rspec's built in matchers:

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

If you're prepared to name your subject then the above tests can be simplified further:

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

A
Amin Ariana

JSON comparison solution

Yields a clean but potentially large Diff:

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

Example of console output from real data:

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

(Thanks to comment by @floatingrock)

String comparison solution

If you want an iron-clad solution, you should avoid using parsers which could introduce false positive equality; compare the response body against a string. e.g:

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

But this second solution is less visually friendly as it uses serialized JSON which would include lots of escaped quotation marks.

Custom matcher solution

I tend to write myself a custom matcher that does a much better job of pinpointing at exactly which recursive slot the JSON paths differ. Add the following to your rspec macros:

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

Example of usage 1:

expect_response(response, :no_content)

Example of usage 2:

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

Example output:

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

Another example output to demonstrate a mismatch deep in a nested array:

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

As you can see, the output tells you EXACTLY where to fix your expected JSON.


D
Damien Roche

If you want to take advantage of the hash diff Rspec provides, it is better to parse the body and compare against a hash. Simplest way I've found:

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

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

Z
Zeke Fast

I found a customer matcher here: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Put it in spec/support/matchers/have_content_type.rb and make sure to load stuff from support with something like this in you spec/spec_helper.rb

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

Here is the code itself, just in case it disappeared from the given link.

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

F
Foram

For Your JSON response you should parse that response for expected results For Instance: parsed_response = JSON.parse(response.body)

You can check other variables which is included in response like

expect(parsed_response["success"]).to eq(true)
expect(parsed_response["flashcard"]).to eq("flashcard expected value")
expect(parsed_response["lesson"]).to eq("lesson expected value")
expect(subject["status_code"]).to eq(201)

I prefer also check keys of JSON response, For Example:

expect(body_as_json.keys).to match_array(["success", "lesson","status_code", "flashcard"])

Here, We can use should matchers For expected results in Rspec