Shared Examples with Minitest

James Wainscoat

I rarely if ever apply DRY principles to the tests I write. What might improve application code does not necessarily improve test code. Tests should be as verbose and repetitive as they need to be. If you can’t look at an individual test and understand exactly what’s going on without looking through several before blocks or nested contexts, then something is probably wrong. It’s better to be a bit more verbose and repeat yourself a little than make your tests unreadable.

That said, sometimes you need to repeat nearly the exact same test over and over again. A good example would be for a before_action in a controller that ensures a user is authenticated.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  def authenticate
    redirect_to sign_in_path unless current_user
  end

end
# app/controllers/monitors_controller.rb
class MonitorsController < ApplicationController

  before_action :authenticate

  def index
    # ...
  end

end

Every action that the before_action runs before needs to be tested.

# test/controllers/monitors_controller_test.rb
describe MonitorsController do

  describe "index" do

    it "requires authentication" do
      session[:user_id] = nil
      get :index
      assert_redirected_to sign_in_path
    end

  end

end

You’d have to repeat almost this exact same test for every action, the only thing that would change is the controller and action being requested. This is a perfect candidate for a shared test.

If you’re familiar with RSpec you might know that it has a feature for this called shared examples. Minitest has no corresponding equivalent because sharing tests is an edge case. This pretty well highlights the fundamental difference between RSpec and Minitest: RSpec is everything you might ever need; Minitest is just what you’ll probably need. Since Minitest has no built-in facility for sharing tests, you can just implement your own.1

# test/test_helper.rb
class Minitest::Spec

  def self.it_requires_authentication(&block)
    it "requires authentication" do
      session[:user_id] = nil
      instance_exec(&block)
      assert_redirected_to sign_in_path
    end
  end

end
# test/controllers/monitors_controller_test.rb
describe MonitorsController do

  describe "index" do

    it_requires_authentication do
      get :index
    end

  end

end

No magic, it’s just Ruby.


  1. I’m defining the class method on Minitest::Spec but where you define the class method depends on what framework you’re using and how you integrated Minitest into it. For Rails, the class you probably need to define it on is ActiveSupport::TestCase.