Làm cách nào để kiểm tra phản hồi JSON bằng RSpec?


145

Tôi có đoạn mã sau trong bộ điều khiển của mình:

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

Trong thử nghiệm bộ điều khiển RSpec của tôi, tôi muốn xác minh rằng một kịch bản nhất định sẽ nhận được phản hồi json thành công vì vậy tôi đã có dòng sau:

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

Mặc dù khi tôi chạy thử nghiệm, tôi gặp lỗi sau:

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

Tôi đang kiểm tra phản hồi không chính xác?

Câu trả lời:


164

Bạn có thể kiểm tra đối tượng phản hồi và xác minh rằng nó chứa giá trị mong đợi:

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

BIÊN TẬP

Thay đổi điều này để postlàm cho nó một chút phức tạp hơn. Đây là một cách để xử lý nó:

 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

Lưu ý rằng mock_modelsẽ không đáp ứng to_json, do đó, stub_modelhoặc một ví dụ mô hình thực sự là cần thiết.


1
Tôi đã thử điều này và không may nó nói rằng nó đã nhận được phản hồi của "". Đây có thể là một lỗi trong bộ điều khiển?
Fizz

Ngoài ra, hành động là 'tạo', nó có quan trọng hơn việc tôi sử dụng một bài đăng thay vì nhận được không?
Fizz

Có, bạn muốn post :createvới một tham số băm hợp lệ.
zetetic

4
Bạn cũng nên xác định định dạng bạn yêu cầu. post :create, :format => :json
Robert Spe Rich

8
JSON chỉ là một chuỗi, một chuỗi các ký tự và thứ tự của chúng có vấn đề. {"a":"1","b":"2"}{"b":"2","a":"1"}không phải là các chuỗi bằng nhau mà ký hiệu các đối tượng bằng nhau. Bạn không nên so sánh các chuỗi nhưng các đối tượng, JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}thay vào đó.
skalee

165

Bạn có thể phân tích cơ thể phản hồi như thế này:

parsed_body = JSON.parse(response.body)

Sau đó, bạn có thể thực hiện các xác nhận của mình đối với nội dung được phân tích cú pháp đó.

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

6
Điều này có vẻ dễ dàng hơn nhiều . Cảm ơn.
tbaums

Đầu tiên, cảm ơn rất nhiều. Một hiệu chỉnh nhỏ: JSON.parse (answer.body) trả về một mảng. ['Foo'] tuy nhiên tìm kiếm khóa trong giá trị băm. Cái được sửa là Parsed_body [0] ['foo'].
CanCeylan

5
JSON.parse chỉ trả về một mảng nếu có một mảng trong chuỗi JSON.
redjohn

2
@PriyankaK nếu nó trả về HTML, thì phản hồi của bạn không phải là json. Hãy chắc chắn rằng yêu cầu của bạn là chỉ định định dạng json.
brentmc79

10
Bạn cũng có thể sử dụng b = JSON.parse(response.body, symoblize_names: true)để bạn có thể truy cập chúng bằng các ký hiệu như sau:b[:foo]
FloatingRock

45

Xây dựng câu trả lời của Kevin Trowbridge

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

21
rspec-rails cung cấp một công cụ đối sánh cho điều này: mong đợi (answer.content_type) .to eq ("application / json")
Dan Garland

4
Bạn không thể sử dụng Mime::JSONthay vì 'application/json'?
FloatingRock

@FloatingRock Tôi nghĩ bạn sẽ cầnMime::JSON.to_s
Edgar Ortega


13

Đơn giản và dễ dàng để làm điều này.

# 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

Bạn cũng có thể định nghĩa một hàm trợ giúp bên trong spec/support/

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

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

và sử dụng json_bodybất cứ khi nào bạn cần để truy cập phản hồi JSON.

Ví dụ, bên trong thông số yêu cầu của bạn, bạn có thể sử dụng nó trực tiếp

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

Một cách tiếp cận khác để kiểm tra chỉ cho phản hồi JSON (không phải nội dung bên trong chứa giá trị mong đợi), là phân tích phản hồi bằng ActiveSupport:

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

Nếu phản hồi không thể phân tích cú pháp JSON, một ngoại lệ sẽ được đưa ra và thử nghiệm sẽ thất bại.


7

Bạn có thể nhìn vào 'Content-Type'tiêu đề để thấy rằng nó là chính xác?

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

1
Đối với render :json => object, tôi tin rằng Rails trả về tiêu đề Kiểu nội dung của 'application / json'.
lightyrs

1
Lựa chọn tốt nhất tôi nghĩ:response.header['Content-Type'].should match /json/
thợ nề

Thích nó bởi vì nó giữ cho mọi thứ đơn giản và không thêm một phụ thuộc mới.
webpapaya

5

Khi sử dụng Rails 5 (hiện vẫn đang trong giai đoạn thử nghiệm), có một phương thức mới, parsed_bodytrên phản hồi thử nghiệm, sẽ trả về phản hồi được phân tích cú pháp như những gì yêu cầu cuối cùng được mã hóa.

Cam kết trên GitHub: https://github.com/rails/rails/commit/eee3534b


Rails 5 đã làm cho nó ra khỏi phiên bản beta, cùng với #parsed_body. Nó chưa được ghi lại, nhưng ít nhất định dạng JSON hoạt động. Lưu ý rằng các khóa vẫn là các chuỗi (thay vì các ký hiệu), vì vậy người ta có thể tìm thấy #deep_symbolize_keyshoặc #with_indifferent_accesshữu ích (tôi thích cái sau).
Franklin Yu

1

Nếu bạn muốn tận dụng lợi thế của hàm băm khác mà Rspec cung cấp, tốt hơn là phân tích cơ thể và so sánh với hàm băm. Cách đơn giản nhất tôi đã tìm thấy:

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

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

1

Giải pháp so sánh JSON

Mang lại một Diff sạch nhưng có khả năng lớn:

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

Ví dụ về đầu ra giao diện điều khiển từ dữ liệu thực:

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

(Cảm ơn bình luận của @floatingrock)

Giải pháp so sánh chuỗi

Nếu bạn muốn một giải pháp vỏ sắt, bạn nên tránh sử dụng các trình phân tích cú pháp có thể đưa ra sự bình đẳng dương tính giả; so sánh cơ thể phản hồi với một chuỗi. ví dụ:

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

Nhưng giải pháp thứ hai này ít thân thiện hơn vì nó sử dụng JSON được tuần tự hóa, bao gồm rất nhiều dấu ngoặc kép được thoát.

Giải pháp so khớp tùy chỉnh

Tôi có xu hướng tự viết cho mình một công cụ đối sánh tùy chỉnh thực hiện công việc xác định chính xác vị trí đệ quy mà các đường dẫn JSON khác nhau tốt hơn nhiều. Thêm phần sau vào macro rspec của bạn:

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

Ví dụ về cách sử dụng 1:

expect_response(response, :no_content)

Ví dụ về cách sử dụng 2:

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

Ví dụ đầu ra:

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

Một ví dụ đầu ra khác để chứng minh sự không khớp sâu trong một mảng lồng nhau:

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

Như bạn có thể thấy, đầu ra cho bạn biết CHÍNH XÁC nơi sửa lỗi JSON dự kiến ​​của bạn.


0

Tôi tìm thấy một đối sánh khách hàng ở đây: https://raw.github.com/gist/917903/92d7101f643e07896659f84609c117c4c279dfad/have_content_type.rb

Đặt nó trong spec / support / matchers / have_content_type.rb và đảm bảo tải công cụ từ hỗ trợ với một cái gì đó như thế này trong bạn spec / spec_helper.rb

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

Đây là mã, chỉ trong trường hợp nó biến mất khỏi liên kết đã cho.

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

Rất nhiều câu trả lời ở trên là một chút lỗi thời, vì vậy đây là một bản tóm tắt nhanh chóng cho một phiên bản RSpec mới hơn (3.8+). Giải pháp này không đưa ra cảnh báo nào từ rubocop-rspec và phù hợp với thực tiễn tốt nhất của rspec :

Một phản hồi JSON thành công được xác định bởi hai điều:

  1. Loại nội dung của phản hồi là application/json
  2. Phần thân của phản hồi có thể được phân tích cú pháp mà không có lỗi

Giả sử rằng đối tượng phản hồi là đối tượng ẩn danh của thử nghiệm, cả hai điều kiện trên có thể được xác thực bằng cách sử dụng các đối sánh tích hợp của 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

Nếu bạn chuẩn bị đặt tên cho chủ đề của mình thì các bài kiểm tra trên có thể được đơn giản hóa hơn nữa:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.