如何使用RSpec检查JSON响应?


145

我的控制器中有以下代码:

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

在我的RSpec控制器测试中,我想验证某个场景确实收到了成功的json响应,因此我有以下内容:

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

尽管在运行测试时出现以下错误:

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

我是否检查响应不正确?

Answers:


164

您可以检查响应对象并验证它是否包含期望值:

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

编辑

将此更改为a post会比较麻烦。这是一种处理方法:

 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

请注意,它mock_model不会响应to_json,因此需要一个stub_model或真实的模型实例。


1
我尝试了此操作,很遗憾它说它得到了“”的响应。这可能是控制器中的错误吗?
Fizz

而且动作是“创建”,这比我使用帖子而不是获取要紧吗?
Fizz

是的,您想要post :create一个有效的参数哈希。
zetetic 2011年

4
您还应该指定所需的格式。post :create, :format => :json
罗伯特·施派克

8
JSON只是一个字符串,一个字符序列及其顺序很重要。 {"a":"1","b":"2"}并且{"b":"2","a":"1"}不是表示相等对象的相等字符串。您不应该比较字符串,JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}而应该比较对象。
skalee 2012年

165

您可以这样解析响应主体:

parsed_body = JSON.parse(response.body)

然后,您可以对已解析的内容进行声明。

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

6
这似乎是很多容易。谢谢。
tbaums 2011年

首先,非常感谢。一个小的更正:JSON.parse(response.body)返回一个数组。但是['foo']在哈希值中搜索键。纠正的是parsed_body [0] ['foo']。
CanCeylan 2012年

5
JSON.parse仅在JSON字符串中存在数组时才返回数组。
redjohn

2
@PriyankaK如果返回HTML,则您的响应不是json。确保您的请求指定了json格式。
brentmc79

10
您还可以使用b = JSON.parse(response.body, symoblize_names: true)以便使用如下符号访问它们:b[:foo]
FloatingRock 2014年



13

简单易行的方式来做到这一点。

# 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

您还可以在内部定义一个辅助函数 spec/support/

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

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

json_body在需要访问JSON响应时使用。

例如,在您的请求规范中,您可以直接使用它

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

仅测试JSON响应(而不是其中的内容包含预期值)的另一种方法是使用ActiveSupport解析响应:

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

如果响应不是可解析的JSON,则将引发异常,并且测试将失败。


7

您可以查看'Content-Type'标题以了解它是否正确?

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

1
对于render :json => object,我相信Rails返回Content-Type头“ application / json”。
lightyrs 2012年

1
我认为最好的选择:response.header['Content-Type'].should match /json/
Bricker

之所以喜欢它,是因为它使事情变得简单,并且没有添加新的依赖项。
webpapaya

5

使用Rails 5(当前仍处于beta)时,parsed_body在测试响应上有一个新方法,该方法将返回解析为最后一个请求编码的响应。

在GitHub上的提交:https : //github.com/rails/rails/commit/eee3534b


Rails 5和一起脱离了beta版本#parsed_body。它尚未记录,但至少可以使用JSON格式。请注意,键仍然是字符串(而不是符号),因此可能会发现其中一个#deep_symbolize_keys#with_indifferent_access有用(我喜欢后者)。
富兰克林于

1

如果要利用Rspec提供的哈希值差异,最好解析正文并将其与哈希值进行比较。我找到的最简单的方法:

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

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

1

JSON比较解决方案

产生干净但可能很大的差异:

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

实际数据的控制台输出示例:

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

(感谢@floatingrock发表评论)

字符串比较解决方案

如果您想要一个固定的解决方案,则应避免使用可能引入错误肯定等式的解析器。将响应主体与字符串进行比较。例如:

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

但是第二种解决方案在视觉上不太友好,因为它使用了序列化的JSON,其中包含许多转义的引号。

定制匹配器解决方案

我倾向于给自己写一个自定义匹配器,它可以更好地查明JSON路径到底在哪个递归槽上不同。将以下内容添加到您的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

用法示例1:

expect_response(response, :no_content)

用法示例2:

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

输出示例:

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

另一个示例输出展示了嵌套数组深处的不匹配:

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

如您所见,输出将告诉您确切的位置来修复期望的JSON。


0

我在这里找到了一个客户匹配器:https : //raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

将其放在spec / support / matchers / have_content_type.rb中,并确保在您的spec / spec_helper.rb文件中添加类似支持的内容

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

这是代码本身,以防万一它从给定链接中消失了。

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

上面的许多答案都有些过时,因此这是RSpec(3.8+)最新版本的快速摘要。该解决方案不会引起rubocop-rspec的警告,并且符合rspec的最佳实践

成功的JSON响应由两件事标识:

  1. 响应的内容类型为 application/json
  2. 响应主体可以正确解析

假设响应对象是测试的匿名主题,则可以使用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

如果您准备命名主题,那么可以进一步简化上述测试:

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.