Как проверить ответ 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

РЕДАКТИРОВАТЬ

Изменение этого на 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
Роберт Шпайхер

8
JSON - это только строка, последовательность символов и их порядок. {"a":"1","b":"2"}и {"b":"2","a":"1"}не равные строки, которые записывают равные объекты. Вы не должны сравнивать строки, но объекты, JSON.parse('{"a":"1","b":"2"}').should == {"a" => "1", "b" => "2"}вместо этого.
Скали

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

Построение ответа Кевина Троубриджа

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

21
rspec-rails обеспечивает соответствие: ожидайте (response.content_type) .to eq ("application / json")
Дэн Гарланд

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 возвращает Content-Type заголовок «приложения / JSON».
зажигалки

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

Если вы хотите воспользоваться преимуществами хеша 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 сравнительное решение

Получает чистый, но потенциально большой Diff:

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.