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

В приложении 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.

Мой план состоял в следующем: request spec:

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 по умолчанию, или мне сначала пришлось бы сказать моему контроллеру сделать это как-то.

Ответы

Ответ 1

Вы хотите использовать 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, я выбрал здесь название для удобочитаемости.

Ответ 2

Возврат 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(), который запрашивает этот параметр, кэширует его результат.

Ответ 3

    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() предоставляет доступ к базовому Hash, который он использует для кэширования его результата, поэтому мы можем напрямую его изменить.

Ответ 4

Отказывание 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(), чтобы мы не модифицировали исходный хэш, который повлиял бы на все примеры.

Ответ 5

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

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

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

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