Testing controller concerns in Rails
Tags: ruby, rails, testing
Published: 2022-10-28 17:14:28

_This post was extracted and adapted from [The Rails and Hotwire Codex](https://railsandhotwirecodex.com)._

[Concerns](https://api.rubyonrails.org/classes/ActiveSupport/Concern.html) are a great way to organize and de-duplicate code in Rails. 

One of my favorite use cases is to abstract everything out of the `ApplicationController` 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 always _mixed 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 which `Authenticate`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 called `Authenticate`. It's usable only within a controller, so we'll need a _test harness_ to test it independently.

Create a `support/` folder under `test/` 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
```

The `TestController` 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.

```ruby
class TestController < ActionController::Base
  def index; end
  def new; end
  def create; end
  def show; end
  def edit; end
  def update; end
  def destroy; end

  private

    def default_render
      render plain: "#{params[:controller]}##{params[:action]}"
    end
end
```

Next, we need a way to draw some test-only routes to point to `TestController` subclasses in test cases. The test routes will be scoped to `/test` so they don't clash with existing routes. 

```ruby
# 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.

module RoutesHelpers
  def draw_test_routes(&block)
    # Don't clear routes when calling `Rails.application.routes.draw`
    Rails.application.routes.disable_clear_and_finalize = true

    Rails.application.routes.draw do
      scope "test" do
        instance_exec(&block)
      end
    end
  end

  def reload_routes!
    Rails.application.reload_routes!
  end
end
```

The `draw_test_routes` helper takes a block which is executed inside the context of `Rails.application.routes.draw`. In essence, it's doing the exact same thing as `config/routes.rb`, but in the context of the test suite.

Files under the `test/` folder in Rails are not _autoloaded_, so these files need to be ``require``d and ``include``d manually.

```ruby
# test/test_helper.rb

# ...

Dir[Rails.root.join("test", "support", "**", "*.rb")].each {
  |f| require f
}

# ...

class ActionDispatch::IntegrationTest
  include RoutesHelpers
end
```

With the test harness in place, we can now write some tests for the `Authenticate` concern. 

```
$ touch test/controllers/concerns/authenticate_test.rb
```

```ruby
require 'test_helper'

class AuthenticateTestsController < TestController
  include Authenticate

  def show
    # ...
  end
end

class AuthenticateTest < ActionDispatch::IntegrationTest
  setup do
    draw_test_routes do
      resource :authenticate_test, only: [:new, :create, :show, :edit]
    end
  end

  teardown do
    reload_routes!
  end

   # ...
   # Test cases go here ...
   # ...
end
```

The test-specific `AuthenticateTestsController` strips away any peripheral functionality and enables us to focus on testing the code in `Authenticate`. It can now be tested just like any other controller! 


_If you liked this post, check out my book, [The Rails and Hotwire Codex](https://railsandhotwirecodex.com), to level-up your Rails and Hotwire skills!_