Проверить код состояния HTTP в одних экзаменах запросов RSpec rails, а в других - на поднятое исключение.

В приложении Rails 4.2.0, протестированном с rspec-rails, я предоставляю веб-API JSON с REST-подобным ресурсом с обязательным атрибутом mand_attr.

Я хотел бы проверить, что этот API отвечает кодом HTTP 400 (BAD REQUEST), когда этот атрибут отсутствует в запросе POST. (См. второй пример удара.) Мой контроллер пытается вызвать этот код HTTP с помощью бросая ActionController::ParameterMissing, как показано в первом примере RSpec ниже.

В других примерах RSpec я хочу, чтобы возникшие исключения были спасены примерами (если они ожидаются) или попали в средство выполнения тестов, чтобы они отображались разработчику (если ошибка неожиданная. ), поэтому я не хочу удалять

  # Raise exceptions instead of rendering exception templates.
  config.action_dispatch.show_exceptions = false

с config/environments/test.rb.

Мой план состоял в том, чтобы иметь что-то вроде следующего в спецификации запроса:

describe 'POST' do
  let(:perform_request) { post '/my/api/my_ressource', request_body, request_header }
  let(:request_header) { { 'CONTENT_TYPE' => 'application/json' } }

  context 'without mandatory attribute' do
    let(:request_body) do
      {}.to_json
    end

    it 'raises a ParameterMissing error' do
      expect { perform_request }.to raise_error ActionController::ParameterMissing,
                                                'param is missing or the value is empty: mand_attr'
    end

    context 'in production' do
      ###############################################################
      # How do I make this work without breaking the example above? #
      ###############################################################
      it 'reports BAD REQUEST (HTTP status 400)' do
        perform_request
        expect(response).to be_a_bad_request
        # Above matcher provided by api-matchers. Expectation equivalent to
        #     expect(response.status).to eq 400
      end
    end
  end

  # Below are the examples for the happy path.
  # They're not relevant to this question, but I thought
  # I'd let you see them for context and illustration.
  context 'with mandatory attribute' do
    let(:request_body) do
      { mand_attr: 'something' }.to_json
    end

    it 'creates a ressource entry' do
      expect { perform_request }.to change(MyRessource, :count).by 1
    end

    it 'reports that a ressource entry was created (HTTP status 201)' do
      perform_request
      expect(response).to create_resource
      # Above matcher provided by api-matchers. Expectation equivalent to
      #     expect(response.status).to eq 201
    end
  end
end

Я нашел два рабочих и одно частично работающее решения, которые я отправлю в качестве ответов. Но я не особо доволен ни одним из них, поэтому, если вы можете придумать что-то получше (или просто другое), я хотел бы увидеть ваш подход! Кроме того, если спецификация запроса неправильный тип спецификации, чтобы проверить это, я бы хотел знать об этом.

Предвижу вопрос

Почему вы тестируете фреймворк Rails, а не только свое приложение Rails? У фреймворка Rails есть собственные тесты!

поэтому позвольте мне ответить на этот вопрос заранее: я чувствую, что я тестирую здесь не сам фреймворк, а то, правильно ли я использую фреймворк. Мой контроллер наследуется не от ActionController::Base, а от ActionController::API, и я не знал, использует ли ActionController::API ActionDispatch::ExceptionWrapper по умолчанию или мне сначала пришлось бы каким-то образом сказать своему контроллеру, чтобы он сделал это.


person das-g    schedule 04.04.2015    source источник


Ответы (5)


Вы хотели бы использовать фильтры RSpec за это. Если вы сделаете это таким образом, изменение Rails.application.config.action_dispatch.show_exceptions будет локальным для примера и не будет мешать другим вашим тестам:

# This configure block can be moved into a spec helper
RSpec.configure do |config|
  config.before(:example, exceptions: :catch) do
    allow(Rails.application.config.action_dispatch).to receive(:show_exceptions) { true }
  end
end

RSpec.describe 'POST' do
  let(:perform_request) { post '/my/api/my_ressource', request_body }

  context 'without mandatory attribute' do
    let(:request_body) do
      {}.to_json
    end

    it 'raises a ParameterMissing error' do
      expect { perform_request }.to raise_error ActionController::ParameterMissing
    end

    context 'in production', exceptions: :catch do
      it 'reports BAD REQUEST (HTTP status 400)' do
        perform_request
        expect(response).to be_a_bad_request
      end
    end
  end
end

exceptions: :catch - это «произвольные метаданные» на языке RSpec, я выбрал здесь название для удобства чтения.

person awendt    schedule 17.02.2016
comment
К сожалению, похоже, что это работает только тогда, когда этот пример выполняется первым. Когда сначала запускаются другие примеры (без флага exceptions: :catch), стек промежуточного программного обеспечения создается с помощью промежуточного программного обеспечения ShowExceptions, и кажется, что этот стек используется совместно между примерами. - person Franz; 07.04.2017
comment
TL; DR Это делает наши тесты зависимыми от порядка. - person Franz; 07.04.2017

Возврат nil из частично смоделированной конфигурации приложения с

    context 'in production' do
      before do
        allow(Rails.application.config.action_dispatch).to receive(:show_exceptions)
      end

      it 'reports BAD REQUEST (HTTP status 400)' do
        perform_request
        expect(response).to be_a_bad_request
      end
    end

или более явно с

    context 'in production' do
      before do
        allow(Rails.application.config.action_dispatch).to receive(:show_exceptions).and_return nil
      end

      it 'reports BAD REQUEST (HTTP status 400)' do
        perform_request
        expect(response).to be_a_bad_request
      end
    end

работал бы, если бы это был единственный запущенный пример. Но если бы это было так, мы могли бы просто отказаться от настройки с config/environments/test.rb, так что это немного спорный вопрос. При наличии нескольких примеров это не сработает, поскольку Rails.application.env_config(), запрашивающий этот параметр, кэширует его результат.

person das-g    schedule 04.04.2015

Мокинг Rails.application.env_config() для возврата измененного результата

    context 'in production' do
      before do
        # We don't really want to test in a production environment,
        # just in a slightly deviating test environment,
        # so use the current test environment as a starting point ...
        pseudo_production_config = Rails.application.env_config.clone

        # ... and just remove the one test-specific setting we don't want here:
        pseudo_production_config.delete 'action_dispatch.show_exceptions'

        # Then let `Rails.application.env_config()` return that modified Hash
        # for subsequent calls within this RSpec context.
        allow(Rails.application).to receive(:env_config).
                                        and_return pseudo_production_config
      end

      it 'reports BAD REQUEST (HTTP status 400)' do
        perform_request
        expect(response).to be_a_bad_request
      end
    end

сделает свое дело. Обратите внимание, что мы clone результат env_config(), чтобы не изменить исходный хэш, который повлияет на все примеры.

person das-g    schedule 04.04.2015

    context 'in production' do
      around do |example|
        # Run examples without the setting:
        show_exceptions = Rails.application.env_config.delete 'action_dispatch.show_exceptions'
        example.run

        # Restore the setting:
        Rails.application.env_config['action_dispatch.show_exceptions'] = show_exceptions
      end

      it 'reports BAD REQUEST (HTTP status 400)' do
        perform_request
        expect(response).to be_a_bad_request
      end
    end

будет делать свое дело, но кажется немного грязным. Это работает, потому что Rails.application.env_config() дает доступ к базовому хешу, который он использует для кэширования своего результата, поэтому мы можем напрямую изменить его.

person das-g    schedule 04.04.2015

На мой взгляд, тест исключения не входит в спецификацию запроса; спецификации запроса обычно предназначены для тестирования вашего api с точки зрения клиента, чтобы убедиться, что весь стек вашего приложения работает должным образом. Они также похожи по своей природе на тесты функций при тестировании пользовательского интерфейса. Так как ваши клиенты не увидят это исключение, вероятно, оно не принадлежит к этому списку.

Я также могу посочувствовать вашей озабоченности по поводу правильного использования фреймворка и вашего желания в этом убедиться, но похоже, что вы слишком вовлекаетесь во внутреннюю работу фреймворка.

Что бы я сделал, так это сначала выяснил, правильно ли я использую эту функцию во фреймворке (это можно сделать с помощью подхода TDD или в виде всплеска); как только я пойму, как достичь того, чего я хочу достичь, я напишу спецификацию запроса, в которой я беру на себя роль клиента и не беспокоюсь о деталях фреймворка; просто проверьте результат с учетом конкретных входных данных.

Мне было бы интересно увидеть код, который вы написали в своем контроллере, потому что его также можно использовать для определения стратегии тестирования. Если вы написали код, вызывающий исключение, это могло бы оправдать его тест, но в идеале это был бы модульный тест для контроллера. Это будет тест контроллера в rspec-rails среде.

person George Taveras    schedule 05.12.2015
comment
Написать пример спецификации контроллера, который проверяет правильность исключения, - тривиальная задача. Однако с настройками теста Rails по умолчанию, пример спецификации запроса также увидит исключение вместо кода состояния HTTP, кроме производственного клиента , что бы увидеть последнее. Я предполагаю, что серьезные мысли были вложены в это значение по умолчанию, поэтому я хотел бы отклоняющуюся настройку теста только для этого одного примера, а не для всех моих спецификаций запроса. - person das-g; 06.12.2015