Search code examples
ruby-on-railsminitest

How to make a test raise a 404 error instead of an ActiveRecord::RecordNotFound exception?


I'm trying to write a test to confirm that my application sends a 404 Not Found in response to a request for an invalid record (say a record with id=0).

My test code looks like this:

get '/pluralized_name_for_my_resource/0', as: :json
assert_response :not_found, "Doesn't return 404 Not Found for invalid id"

If I plug the corresponding URL into a browser, e.g. http://localhost:3000/pluralized_name_for_my_resource/0.json, the app raises the activerecord exception internally (and prints a message to the logs about it) but correctly responds with a 404 Not Found.

When I run the test, however, the exception isn't handled the same way. The test aborts and returns in the first get '/pluralized_... line and is reported as an error rather than a passing test.

How can I make my test environment handle the exception in the same way as my development environment, so that my test passes?


Solution

  • I did a bit of experimenting and given rails g scaffold foo and this test:

    require "test_helper"
    
    class FoosControllerTest < ActionDispatch::IntegrationTest
      setup do
        @foo = foos(:one)
      end
    
      test "should respond with not found" do
        get foo_url('xxxxxx'), as: :json
        assert_response :not_found
        assert_equal({ "status" => 404, "error"=>"Not Found"}, response.parsed_body)
      end
    end
    

    With the default settings the ActiveRecord::RecordNotFound exception is indeed not caught by the exception handler and bubbles up to the test.

    Error:
    FoosControllerTest#test_should_respond_with_not_found:
    ActiveRecord::RecordNotFound: Couldn't find Foo with 'id'=xxxxxx
        app/controllers/foos_controller.rb:63:in `set_foo'
        test/controllers/foos_controller_test.rb:9:in `block in <class:FoosControllerTest>'
    

    What seems to work is changing config.consider_all_requests_local and config.action_dispatch.show_exceptions in config/environments/test.rb:

    # Show full error reports and disable caching.
    config.consider_all_requests_local       = false # was true
    config.action_controller.perform_caching = false
    config.cache_store = :null_store
    
    # Raise exceptions instead of rendering exception templates.
    config.action_dispatch.show_exceptions = true # was false
    

    With those two changes the test passes:

    Running 1 tests in a single process (parallelization threshold is 50)
    Run options: --seed 27622
    
    # Running:
    
    .
    
    Finished in 0.270401s, 3.6982 runs/s, 7.3964 assertions/s.
    1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
    

    However doing so might not be the best idea since its often useful that an exception in the application causes your tests to bail early. A hacky workaround is to use assert_raise to catch the exception when it bubbles up to the test:

    require "test_helper"
    
    class FoosControllerTest < ActionDispatch::IntegrationTest
      setup do
        @foo = foos(:one)
      end
    
      test "should respond with not found" do
        assert_raise(ActiveRecord::RecordNotFound) do
          get foo_url('xxxxxx'), as: :json
          assert_response :not_found
          assert_equal({ "status" => 404, "error"=>"Not Found"}, response.parsed_body)
        end
      end
    end