This post was extracted and adapted fromThe Rails and Hotwire Codex.
Concerns are a great way to organize and de-duplicate code in Rails.
One of my favorite use cases is to abstract everything out of theApplicationController
into concerns. This begs the question:how do you test these concerns?
The approach for testing concerns is a bit of a judgement call as they're alwaysmixed in with a class and aren't used on their own. Ideally we'd write tests to verify the behavior of a class which would also test the concern in the bargain.
But what if the concern adds the same functionality to a large number of classes? For example, a concern whichAuthenticate
s every request. It isn't pragmatic to test each and every controller action for authentication logic. In such cases, I believe the best approach is to test the concern in isolation.
Test harness for controller concerns
Let's stick with the hypothetical example of a concern calledAuthenticate
. It's usable only within a controller, so we'll need atest harness to test it independently.
Create asupport/
folder undertest/
and create some files for the test harness within it.
$ mkdir test/support$ touch test/support/test_controller.rb$ touch test/support/routes_helper.rb
TheTestController
doesn't need to do much by itself. It will be subclassed in individual tests. Each action will render the name of the controller and action which can then be asserted in the test cases.
classTestController<ActionController::Basedefindex;enddefnew;enddefcreate;enddefshow;enddefedit;enddefupdate;enddefdestroy;endprivatedefdefault_renderrenderplain:"#{params[:controller]}##{params[:action]}"endend
Next, we need a way to draw some test-only routes to point toTestController
subclasses in test cases. The test routes will be scoped to/test
so they don't clash with existing routes.
# test/support/routes_helper.rb# Ensure you call `reload_routes!` in your test's `teardown`# to erase all the routes drawn by your test case.moduleRoutesHelpersdefdraw_test_routes(&block)# Don't clear routes when calling `Rails.application.routes.draw`Rails.application.routes.disable_clear_and_finalize=trueRails.application.routes.drawdoscope"test"doinstance_exec(&block)endendenddefreload_routes!Rails.application.reload_routes!endend
Thedraw_test_routes
helper takes a block which is executed inside the context ofRails.application.routes.draw
. In essence, it's doing the exact same thing asconfig/routes.rb
, but in the context of the test suite.
Files under thetest/
folder in Rails are notautoloaded, so these files need to berequire
d andinclude
d manually.
# test/test_helper.rb# ...Dir[Rails.root.join("test","support","**","*.rb")].each{|f|requiref}# ...classActionDispatch::IntegrationTestincludeRoutesHelpersend
With the test harness in place, we can now write some tests for theAuthenticate
concern.
$ touch test/controllers/concerns/authenticate_test.rb
require'test_helper'classAuthenticateTestsController<TestControllerincludeAuthenticatedefshow# ...endendclassAuthenticateTest<ActionDispatch::IntegrationTestsetupdodraw_test_routesdoresource:authenticate_test,only:[:new,:create,:show,:edit]endendteardowndoreload_routes!end# ...# Test cases go here ...# ...end
The test-specificAuthenticateTestsController
strips away any peripheral functionality and enables us to focus on testing the code inAuthenticate
. It can now be tested just like any other controller!
If you liked this post, check out my book,The Rails and Hotwire Codex, to level-up your Rails and Hotwire skills!
Top comments(1)

- LocationRiga, Latvia
- EducationCollege, Unfinished
- WorkSoftware Engineer at UPB AS
- Joined
An excellent writeup, Ayush!
Testing mixins in isolation, as if we were writing them in a gem without any real implementers yet, is key and should be applied irrespective of the number of places used, as I expound indev.to/epigene/the-alternative-to-...
One avenue of improvement, especially in RSpec context, would be to avoid defining a named class withclass AuthenticateTestsController < TestController
which pollutes the object space and can lead to subtle bugs due to collision, and instead define an anonymous class and usestub_const
to attach the class to the constant for just that one example.
For further actions, you may consider blocking this person and/orreporting abuse