I'm trying to sort out a weird behavior in Rails using the Action Controller bug report template.
For the record, this is the controller in the template:
class TestController < ActionController::Base
include Rails.application.routes.url_helpers
def index
render text: 'Home'
end
end
I've added a route to a missing action:
routes.draw do
get '/' => 'test#index'
get '/missing' => 'test#missing'
end
and I'm trying to assert that AbstractController::ActionNotFound
is raised when following that route:
class BugTest < Minitest::Test
include Rack::Test::Methods
def test_missing
assert_raises(AbstractController::ActionNotFound) { get '/missing' }
end
private
def app
Rails.application
end
end
Expected behavior: green test.
Actual behavior:
# Running tests:
D, [2014-04-24T09:17:41.948985 #4497] DEBUG -- :
D, [2014-04-24T09:17:41.949029 #4497] DEBUG -- :
I, [2014-04-24T09:17:41.949094 #4497] INFO -- : Started GET "/missing" for 127.0.0.1 at 2014-04-24 09:17:41 +0200
F, [2014-04-24T09:17:41.951337 #4497] FATAL -- :
AbstractController::ActionNotFound (The action 'missing' could not be found for TestController):
[stacktrace]
1) Failure:
BugTest#test_missing [action_controller_gem.rb:45]:
AbstractController::ActionNotFound expected but nothing was raised.
So, basically, the exception is raised but Minitest is claiming nothing was raised. I am stumped here.
Why can't Minitest assert that AbstractController::ActionNotFound
is raised? I've looked into Rack::Test::Methods
to rule out that get
works with threads or something but couldn't find anything.
I've also looked at the implementation of assert_raises
-- nothing obvious.
Why doesn't assert_raises(AbstractController::ActionNotFound) { get '/missing' }
pass?
(Never mind the fact that this is already tested in Rails. I'm trying to get past this for the real deal.)
We can run all of our tests at once by using the bin/rails test command. Or we can run a single test file by passing the bin/rails test command the filename containing the test cases. This will run all test methods from the test case.
The currently accepted way to test rails controllers is by sending http requests to your application and writing assertions about the response. Rails has ActionDispatch::IntegrationTest which provides integration tests for Minitest which is the Ruby standard library testing framework.
assert_raise(exception, function) Asserts the exception is raised during function execution. Returns the rescued exception, fails otherwise. assert_raise(exception, message, function) Asserts the exception is raised during function execution with the expected message , which can be a Regex or an exact String .
This isn't a problem with assert_raises
, that continues to work just fine. The issue you are having is that the exception you are raising within your Rails app is getting handled by your Rails app and not propagating out to your test. Calling get '/missing'
is raising the AbstractController::ActionNotFound
error, but then your app handles the error and returns an appropriate response (404 or 500) to the client (your test).
Okay, so how would you go about testing that your controller really did raise the error you are expecting? Normally you would just use an ActionController::TestCase
test to test your controllers. But, when you call an action on a controller that doesn't in ActionController::TestCase
you get a different error. So the following:
require "test_helper"
class TestControllerTest < ActionController::TestCase
def test_missing
assert_raises AbstractController::ActionNotFound do
get :missing
end
end
end
Produces the following output:
1) Failure:
TestControllerTest#test_missing [test/controllers/test_controller_test.rb:6]:
[AbstractController::ActionNotFound] exception expected, not
Class: <ActionController::UrlGenerationError>
Message: <"No route matches {:action=>\"missing\", :controller=>\"test\"}">
---Backtrace---
test/controllers/test_controller_test.rb:7:in `block in test_missing'
test/controllers/test_controller_test.rb:6:in `test_missing'
---------------
The reason is because ActionController::TestCase
is aware of the routes and won't allow calling an action that is not in the routes. But that is exactly what you are trying to test. And, I assume why you aren't using ActionController::TestCase
.
At this point I wonder if you aren't testing something you shouldn't. Perhaps you should allow Rails to do its job and trust that it behaves correctly. But hey, we've come this far, why not just go all the way and test what Rails tests anyway. Instead of making the call through the full Rails app, let's try calling the TestController
directly. If we create a new instance of the controller we can ask it to handle an action even if that action is not defined on the controller. But to do this we will have to dive deep into how ActionController works and make use of the AbstractController#process
method:
require "test_helper"
class TestControllerTest < Minitest::Test
def test_missing
assert_raises AbstractController::ActionNotFound do
TestController.new.process :missing
end
end
end
Now we are asking the controller to process an action that doesn't exist and testing that the controller behaves as expected. Yay?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With