Як перевірити відповідь 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

EDIT

Якщо змінити цей режим 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
Я спробував це, і на жаль, він говорить, що отримав відповідь "". Чи може це бути помилкою в контролері?
Фізз

Також дія є "створити", чи важливо, ніж я використовую пост замість отримання?
Фізз

Так, ви хочете post :createмати дійсний хеш параметрів.
зететик

4
Ви також повинні вказати формат, який ви запитуєте. post :create, :format => :json
Роберт Шпейхер

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'] проте шукає ключ у хеш-значенні. Виправлений параметр_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

Будівництво відповіді Кевіна Троубриджа

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 повертає заголовок типу "Зміст / json".
lightyrs

1
Я вважаю найкращим варіантом:response.header['Content-Type'].should match /json/
муляр

Подобається, тому що він просто робить речі і не додає нової залежності.
webpapaya

5

При використанні Rails 5 (наразі все ще знаходиться в бета-версії) є новий метод parsed_bodyтестової відповіді, який поверне відповідь, проаналізовану як те, на що було закодовано останній запит.

Комісія на GitHub: https://github.com/rails/rails/commit/eee3534b


Rails 5 зробив це з бета-версії разом з #parsed_body. Це ще не документально підтверджено, але принаймні формат JSON працює. Зауважте, що ключі все ще є рядками (замість символів), тому можна знайти #deep_symbolize_keysабо #with_indifferent_accessкорисні (останній мені подобається).
Франклін Ю

1

Якщо ви хочете скористатися хеш-програмою, яку пропонує 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)

Рішення для порівняння рядків

Якщо ви хочете, щоб обшиті залізом рішення, вам слід уникати використання аналізаторів, які можуть ввести помилкову позитивну рівність; порівняйте тіло відповіді з рядком. наприклад:

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.


0

Тут я знайшов відповідного клієнта: 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

0

Багато згаданих вище відповідей трохи застаріли, тому це короткий підсумок новітньої версії RSpec (3.8+). Це рішення не викликає попереджень від rubocop-rspec і узгоджується з найкращими методами 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.