Test for HTTP status code in some RSpec rails request exampes, but for raised exception in others

喜欢而已 提交于 2019-12-08 14:37:54

问题


In a Rails 4.2.0 application tested with rspec-rails, I provide a JSON web API with a REST-like resource with a mandatory attribute mand_attr.

I'd like to test that this API answers with HTTP code 400 (BAD REQUEST) when that attribute is missing from a POST request. (See second example blow.) My controller tries to cause this HTTP code by throwing an ActionController::ParameterMissing, as illustrated by the first RSpec example below.

In other RSpec examples, I want raised exceptions to be rescued by the examples (if they're expected) or to hit the test runner, so they're displayed to the developer (if the error is unexpected), thus I do not want to remove

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

from config/environments/test.rb.

My plan was to have something like the following in a 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

I have found two working and one partially working solutions which I'll post as answers. But I'm not particularly happy with any of them, so if you can come up with something better (or just different), I'd like to see your approach! Also, if a request spec is the wrong type of spec to test this, I'd like to know so.

I foresee the question

Why are you testing the Rails framework instead of just your Rails application? The Rails framework has tests of its own!

so let me answer that pre-emptively: I feel I'm not testing the framework itself here, but whether I'm using the framework correctly. My controller doesn't inherit from ActionController::Base but from ActionController::API and I didn't know whether ActionController::API uses ActionDispatch::ExceptionWrapper by default or whether I first would have had to tell my controller to do so somehow.


回答1:


You'd want to use RSpec filters for that. If you do it this way, the modification to Rails.application.config.action_dispatch.show_exceptions will be local to the example and not interfere with your other tests:

# 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

The exceptions: :catch is "arbitrary metadata" in RSpec speak, I chose the naming here for readability.




回答2:


Returning nil from a partially mocked application config with

    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

or more explicitly with

    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

would work if that was the only example being run. But if it was, we could just as well drop the setting from config/environments/test.rb, so this is a bit moot. When there are several examples, this will not work, as Rails.application.env_config(), which queries this setting, caches its result.




回答3:


Mocking Rails.application.env_config() to return a modified result

    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

will do the trick. Note that we clone the result from env_config(), lest we modify the original Hash which would affect all examples.




回答4:


    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

will do the trick, but feels kinda dirty. It works because Rails.application.env_config() gives access to the underlying Hash it uses for caching its result, so we can directly modify that.




回答5:


In my opinion the exception test does not belong in a request spec; request specs are generally to test your api from the client's perspective to make sure your whole application stack is working as expected. They are also similar in nature to feature tests when testing a user interface. So because your clients won't be seeing this exception, it probably does not belong there.

I can also sympathize with your concern about using the framework correctly and wanting to make sure of that, but it does seem like you are getting too involved with the inner workings of the framework.

What I would do is first figure out whether I am using the feature in the framework correctly, (this can be done with a TDD approach or as a spike); once I understand how to accomplish what I want to accomplish, I'd write a request spec where I take the role of a client, and not worry about the framework details; just test the output given specific inputs.

I'd be interested to see the code that you have written in your controller because this can also be used to determine the testing strategy. If you wrote the code that raises the exception then that might justify a test for it, but ideally this would be a unit test for the controller. Which would be a controller test in an rspec-rails environment.



来源:https://stackoverflow.com/questions/29445756/test-for-http-status-code-in-some-rspec-rails-request-exampes-but-for-raised-ex

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!