จะตรวจสอบการตอบสนอง JSON โดยใช้ RSpec ได้อย่างไร?


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

ฉันตรวจสอบคำตอบอย่างไม่ถูกต้องหรือไม่?

คำตอบ:


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

4
คุณควรระบุรูปแบบที่คุณต้องการ post :create, :format => :json
Robert Speicher

8
JSON เป็นเพียงสตริงลำดับของตัวละครและลำดับของมันมีความสำคัญ {"a":"1","b":"2"}และ{"b":"2","a":"1"}ไม่ใช่สตริงที่เท่ากันซึ่งสังเกตเห็นวัตถุที่เท่ากัน คุณไม่ควรเปรียบเทียบสตริง แต่วัตถุให้ทำJSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}แทน
skalee

165

คุณสามารถแยกวิเคราะห์เนื้อหาการตอบสนองเช่นนี้:

parsed_body = JSON.parse(response.body)

จากนั้นคุณสามารถทำการยืนยันกับเนื้อหาที่แยกวิเคราะห์

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

6
นี้ดูเหมือนเป็นจำนวนมากได้ง่ายขึ้น ขอบคุณ
tbaums

ก่อนอื่นขอบคุณมาก การแก้ไขเล็กน้อย: JSON.parse (response.body) ส่งคืนอาร์เรย์ ['foo'] อย่างไรก็ตามค้นหาคีย์ด้วยค่าแฮช รายการที่แก้ไขคือ parsed_body [0] ['foo']
CanCeylan

5
JSON.parse ส่งกลับอาร์เรย์เฉพาะในกรณีที่มีอาร์เรย์ในสตริง JSON
redjohn

2
@PriyankaK หากมีการส่งคืน HTML แสดงว่าคำตอบของคุณไม่ใช่ json ตรวจสอบให้แน่ใจว่าคำขอของคุณกำลังระบุรูปแบบ json
brentmc79

10
คุณยังสามารถใช้b = JSON.parse(response.body, symoblize_names: true)เพื่อให้สามารถเข้าถึงได้โดยใช้สัญลักษณ์ดังนี้:b[:foo]
FloatingRock

45

การสร้างคำตอบของKevin Trowbridge

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

21
rspec-rails ให้ผู้จับคู่สำหรับสิ่งนี้: คาดหวัง (response.content_type) .to eq ("application / json")
Dan Garland

4
คุณไม่สามารถใช้Mime::JSONแทนได้'application/json'ใช่ไหม
FloatingRock

@FloatingRock ฉันคิดว่าคุณจะต้องMime::JSON.to_s
เอ็ดการ์กาซา


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 คืนค่าส่วนหัวของประเภทเนื้อหาของ 'application / json'
lightyrs

1
ตัวเลือกที่ดีที่สุดที่ฉันคิดว่า:response.header['Content-Type'].should match /json/
bricker

ชอบเพราะมันทำให้สิ่งต่าง ๆ เรียบง่ายและไม่เพิ่มการพึ่งพาใหม่
webpapaya

5

เมื่อใช้ Rails 5 (ปัจจุบันยังอยู่ในรุ่นเบต้า) มีวิธีการใหม่parsed_bodyในการตอบสนองการทดสอบซึ่งจะส่งกลับการตอบสนองแยกวิเคราะห์ว่าสิ่งที่ร้องขอสุดท้ายถูกเข้ารหัสที่

ความมุ่งมั่นใน GitHub: https://github.com/rails/rails/commit/eee3534b


ราง 5 #parsed_bodyทำให้มันออกจากรุ่นเบต้าพร้อมกับ ยังไม่ได้ทำเป็นเอกสาร แต่อย่างน้อยรูปแบบ JSON จะทำงานได้ โปรดทราบว่าปุ่มยังคงเป็นสตริง (แทนที่จะเป็นสัญลักษณ์) ดังนั้นหนึ่งอาจพบว่ามีประโยชน์#deep_symbolize_keysหรืออย่างใดอย่างหนึ่ง#with_indifferent_access(ฉันชอบหลัง)
Franklin Yu

1

หากคุณต้องการใช้ประโยชน์จากแฮช diff 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)

วิธีการเปรียบเทียบสตริง

หากคุณต้องการวิธีแก้ปัญหาที่หุ้มด้วยเหล็กคุณควรหลีกเลี่ยงการใช้ parsers ซึ่งอาจนำไปสู่ความเท่าเทียมในเชิงบวกที่ผิดพลาด เปรียบเทียบเนื้อหาการตอบสนองกับสตริง เช่น:

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

แต่โซลูชันที่สองนี้มีความเป็นมิตรต่อสายตาน้อยกว่าเนื่องจากใช้ JSON แบบอนุกรมซึ่งจะมีเครื่องหมายคำพูดที่ใช้ Escape จำนวนมาก

โซลูชัน matcher ที่กำหนดเอง

ฉันมักจะเขียน matcher แบบกำหนดเองที่ทำงานได้ดีขึ้นมากในการหาตำแหน่งที่สล็อตแบบเรียกซ้ำที่พา ธ 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

วางไว้ในข้อมูลจำเพาะ / 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 ที่สร้างขึ้นใน matchers:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.