如何使用 RSpec 检查 JSON 响应?
Posted
技术标签:
【中文标题】如何使用 RSpec 检查 JSON 响应?【英文标题】:How to check for a JSON response using RSpec? 【发布时间】:2011-07-06 19:16:46 【问题描述】:我的控制器中有以下代码:
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
我检查的响应有误吗?
【问题讨论】:
【参考方案1】:您可以检查响应对象并验证它是否包含预期值:
@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
或真实模型实例。
【讨论】:
我试过这个,不幸的是它说它得到了“”的响应。这可能是控制器中的错误吗? 另外,动作是“创建”,这比我使用帖子而不是获取重要吗? 是的,您希望post :create
具有有效的参数哈希。
您还应该指定您请求的格式。 post :create, :format => :json
JSON 只是一个字符串,一个字符序列,它们的顺序很重要。 "a":"1","b":"2"
和 "b":"2","a":"1"
不是表示相等对象的相等字符串。您不应该比较字符串而是对象,而是使用JSON.parse('"a":"1","b":"2"').should == "a" => "1", "b" => "2"
。【参考方案2】:
您可以像这样解析响应正文:
parsed_body = JSON.parse(response.body)
然后您可以针对已解析的内容做出断言。
parsed_body["foo"].should == "bar"
【讨论】:
这似乎很多容易。谢谢。 首先,非常感谢。一个小的更正: JSON.parse(response.body) 返回一个数组。 ['foo'] 但是在哈希值中搜索键。更正后的是 parsed_body[0]['foo']。 JSON.parse 仅在 JSON 字符串中有数组时才返回数组。 @PriyankaK 如果它返回 html,那么您的响应不是 json。确保您的请求指定了 json 格式。 您也可以使用b = JSON.parse(response.body, symoblize_names: true)
,这样您就可以使用如下符号访问它们:b[:foo]
【参考方案3】:
还有json_spec gem,值得一看
https://github.com/collectiveidea/json_spec
【讨论】:
这个库还包括看起来非常有用的 Cucumber 步骤定义。【参考方案4】:您可以查看'Content-Type'
标头是否正确?
response.header['Content-Type'].should include 'text/javascript'
【讨论】:
对于render :json => object
,我相信Rails会返回'application/json'的Content-Type标头。
我认为最好的选择:response.header['Content-Type'].should match /json/
喜欢它,因为它使事情变得简单并且不会添加新的依赖项。【参考方案5】:
以Kevin Trowbridge's answer为基础
response.header['Content-Type'].should include 'application/json'
【讨论】:
rspec-rails 为此提供了一个匹配器:expect(response.content_type).to eq("application/json") 你不能用Mime::JSON
代替'application/json'
吗?
@FloatingRock 我想你需要Mime::JSON.to_s
【参考方案6】:
我在这里找到了一个客户匹配器: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
【讨论】:
【参考方案7】:另一种仅测试 JSON 响应(不是其中的内容包含预期值)的方法是使用 ActiveSupport 解析响应:
ActiveSupport::JSON.decode(response.body).should_not be_nil
如果响应是不可解析的 JSON,则会抛出异常并且测试将失败。
【讨论】:
【参考方案8】:简单易行的方法。
# 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
【讨论】:
【参考方案9】:在使用 Rails 5(目前仍处于测试阶段)时,测试响应上有一个新方法 parsed_body
,它将返回解析为最后一个请求编码的响应。
GitHub 上的提交:https://github.com/rails/rails/commit/eee3534b
【讨论】:
Rails 5 和#parsed_body
一起退出了测试版。它尚未记录,但至少 JSON 格式有效。请注意,键仍然是字符串(而不是符号),因此可能会发现 #deep_symbolize_keys
或 #with_indifferent_access
有用(我喜欢后者)。【参考方案10】:
你也可以在spec/support/
中定义一个辅助函数
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
【讨论】:
【参考方案11】:如果您想利用 Rspec 提供的散列差异,最好解析主体并与散列进行比较。我发现的最简单的方法:
it 'asserts json body' do
expected_body =
my: 'json',
hash: 'ok'
.stringify_keys
expect(JSON.parse(response.body)).to eql(expected_body)
end
【讨论】:
【参考方案12】:上面的很多答案都有点过时了,所以这是对 RSpec (3.8+) 更新版本的快速总结。此解决方案不会从rubocop-rspec 发出警告,并且与rspec best practices 内联:
一个成功的 JSON 响应由两件事来识别:
-
响应的内容类型是
application/json
可以正确解析响应正文
假设响应对象是测试的匿名主体,上述两个条件都可以使用 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
【讨论】:
【参考方案13】: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。
【讨论】:
【参考方案14】:对于您的 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"])
在这里,我们可以使用 should matchers 来获得 Rspec 中的预期结果
【讨论】:
以上是关于如何使用 RSpec 检查 JSON 响应?的主要内容,如果未能解决你的问题,请参考以下文章
为啥在测试 RSPEC 时 JBuilder 不返回 JSON 中的响应正文