Shared Examples with Minitest
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.
-
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 isActiveSupport::TestCase
. ↩