【问题标题】:How to check for a JSON response using RSpec?如何使用 RSpec 检查 JSON 响应?
【发布时间】: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

我检查的响应有误吗?

【问题讨论】:

    标签: ruby-on-rails json rspec


    【解决方案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 =&gt; :json
    • JSON 只是一个字符串,一个字符序列,它们的顺序很重要。 {"a":"1","b":"2"}{"b":"2","a":"1"} 不是表示相等对象的相等字符串。您不应该比较字符串而是对象,而是使用JSON.parse('{"a":"1","b":"2"}').should == {"a" =&gt; "1", "b" =&gt; "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 =&gt; 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

          【讨论】:

          【解决方案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 响应由两件事来识别:

              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
              

              【讨论】:

                【解决方案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 中的预期结果

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-08-09
                    • 1970-01-01
                    • 2017-07-10
                    • 1970-01-01
                    • 2021-11-19
                    • 2019-05-05
                    • 2013-06-07
                    相关资源
                    最近更新 更多