如何使用 RSpec 检查 JSON 响应?

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

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

我是不是检查错了?

190956 次浏览

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

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

剪辑

把它改成 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或实际的模型实例。

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

parsed_body = JSON.parse(response.body)

然后您可以针对解析后的内容进行断言。

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

还有 Json _ spec宝石,值得一看

Https://github.com/collectiveidea/json_spec

你可以查看 'Content-Type'头部,看看它是正确的吗?

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

基于 Kevin Trowbridge 的回答的建筑

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

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

把它放在 spec/support/matchers/have _ content _ type. rb 中,并确保用 spec/spec _ helper. rb 中的类似内容从 support 中加载内容

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

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

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

如果响应不是可解析的 JSON,就会抛出异常,测试就会失败。

做这件事的简单易行的方法。

# 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

在使用 Rails 5(目前还处于 beta 测试阶段)时,测试响应中有一个新方法 parsed_body,它将返回按照最后一个请求的编码位置进行解析的响应。

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

您还可以在 spec/support/中定义一个 helper 函数

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


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

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

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

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

如果希望利用 Rspec 提供的 hash diff,最好解析主体并与 hash 进行比较。我找到的最简单的方法:

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


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

上面的许多答案都有点过时了,所以这是对 RSpec (3.8 +)最新版本的一个快速总结。这个解决方案不会引起来自 机械警察的警告,并且与 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

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

例子一:

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。

对于您的 JSON 响应,您应该解析该响应以获得预期的结果 例如: parsed_response = JSON.parse(response.body)

您可以检查响应中包含的其他变量,如

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)

我更喜欢检查 JSON 响应的键,例如:

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

在这里,我们可以使用在 Rspec 的预期结果