Minitest Masterclass (Part 1: Basic Ruby-only Setup, No Rails)

Minitest is a powerful testing framework for testing Ruby and Ruby on Rails apps. It has a clear, expressive style and is a more elite tool than its sibling testing library, Rspec.

What are the differences between Minitest and Rspec?

MinitestRspec
Style assert-style syntax
assert_equal(expected, thing_to_test)
spec-style syntax
expect(thing_to_test).to be(expected)
Ease of UseSimple to useSimple to use
Writing StylePrefers Ruby-ish style conventions for code re-use — use inheritance, modules, helpers, etc. Prefers Rspec’s DSL for code re-use, makes heavy use of nested context blocks
Getting Started Many extensions make it confusing to get started or to understand which extension is right for which purpose.Most core functionality is built-in with a few extensions for mocking.
Speed to runVery fastSlower because matches create objects that need to be garbage collected
Code vs. MagicMore code and less magicMore magic and less code
Customer matchersEncouragedNot encouraged

Once you start with one, you will generally stay with that choice for the life of the app, so it is important to understand up-front the choices you are making.

Unfortunately, Stack Overflow is filled with either bad or useless advice about setting up Minitest, and the nuances between the different gems are not carefully explained in most of the Stack Overflow posts you will find. Most apps are set up by a single architect or founding engineer. The engineers who join the team just copy what they already see in the codebase, without gaining a solid understanding of the fundamentals of the various Minitest options available.

This guide will introduce you to minitest, minitest-spec, minitest-rails, and minitest-rails-capybara and show you the specific syntax differences between the different flavors of Minitest.

For demonstration purposes we’ll start with a bare-bones Ruby app. Then we’ll throw that away and do it for a brand new Rails app.

1-1/ Using Minitest Outside of Rails

Make a new folder called

mkdir MinitestBasic

cd MinitestBasic

Initialize bundler with:

bundle init

Your project now contains only two files:

The Gemfile has simply this content. (Bunder actually adds a commented-out Gem for “rails” — I’m not sure why— I’ve removed it.)

# frozen_string_literal: true
source "https://rubygems.org"
gem "minitest", "~> 5.18"

Now create a folder called test/ and a file called the_truth.rb

Remember, we’re just doing this for demonstration purposes.

# test/the_truth.rb

require 'minitest/autorun'

class TheTruth < Minitest::Test
  def test_the_truth
    assert_equal(2, 1+1)
  end
end

Here, we are simply asserting that 1+1 = 2. Notice that the assert takes two arguments (actually three, which we’ll introduce later): First, the expected value, and second, the thing you are testing.

Although it might seem to read backward, this is the standard format:

In Minitest, the expected result comes first, followed by the code you are testing (or the actual result).

Run your test using ruby test/the_truth.rb

Now, as an experiment, rename the test method test_the_truth to one_plus_one_equals_two

class TheTruth < Minitest::Test
  def one_plus_one_equals_two
    assert_equal(2, 1+1)
  end
end

If you run ruby test/the_truth.rb, you’ll see no examples are run. That’s because we renamed our test and removed the word “test_” from the start of it.

% ruby test/the_truth.rb
Run options: --seed 17451

# Running:
Finished in 0.000288s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips

This demonstrates that by default, for your test to be picked up by Minitest, you’ll need two key elements:

  1. The test must be a subclass of Minitest::Test *
  2. The method name of the test must begin with test_

*Soon we’ll see that there are other base classes we will descend from, but under the hood, they all descend from the superclass Minitest::Test.

Before continuing, change your test back to using the full method name test_the_truth so it will run again:

def test_one_plus_one_equals_two
  assert_equal(2, 1+1)
end

1-2/ Using a Rakefile

Notice that in the above example, we used a test file called test/the_truth.rb. We covered the requirement your test method names must begin with test_, but we didn’t talk about your filename. That’s because we ran our test explicitly using ruby test/the_truth.rb

In most Ruby projects, you will also want your filenames to end with _test.rb, but that requirement is imposed by the Rakefile setup we’re about to do. Using a Rakefile, we can setup a default rake task to run all of the tests in our test/ folder

Make a new file Rakefile (with a capital R and no extension), like so:

require "rake/testtask"

Rake::TestTask.new do |t|
  t.libs << "test"
  t.pattern = "test/**/*.rb"
end

task :default => :test

Here, we’re creating a new Rake task that will run as default. By “running as default” we mean that unlike other Rake tasks, where you have to specify the name of the task (such as rake xyz), we only need to type rake to run our tests. (That’s what the :default => :test part does).

We are telling the Rake task the test folder is where to look and that we should run any file that matches the pattern test/**/*.rb (so basically any Ruby file inside the test/ directory or its subdirectories).

Indeed, if we run just rake now on the command prompt, we’ll run our specs:

% rake

Run options: --seed 63079

# Running:

.

Finished in 0.000344s, 2906.9861 runs/s, 2906.9861 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

A typical Rake setup does not run every Ruby file inside of the test folder, because that folder often contains test support files that we don’t want to consider test runs.

To get around this problem, let’s make three small changes:

1) change the line t.pattern = "test/**/*.rb" to t.pattern = "test/**/*_test.rb"

2) rename the file itself from the_truth.rb to the_truth_test.rb.

3) Also rename the class from TheTruth to TheTruthTest to match the filename:

test/the_truth_test.rb

# test/the_truth_test.rb

require 'minitest/autorun'

class TheTruthTest < Minitest::Test
  def test_one_plus_one_equals_two
    assert_equal(2, 1+1)
  end
end

Because of the Rakefile, we now have 3 rules to remember for what determines which test will run:

  1. The test must be a subclass of Minitest::Test
  2. The method name of each test must begin with test_
  3. The file name of each of our test files must end with _test.rb

Keep these 3 rules in mind moving forward. This is especially important when you write a test that Minitest doesn’t seem to run because it probably means you forgot one of the three items above.

Rake task modifiers

MT_LIB_EXTRAS :: Extra libs to dynamically override/inject for custom runs.
N             :: -n: Tests to run (string or /regexp/).
X             :: -x: Tests to exclude (string or /regexp/).
A             :: Any extra arguments. Honors shell quoting.
MT_CPU        :: How many threads to use for parallel test runs
SEED          :: -s --seed Sets random seed.
TESTOPTS      :: Deprecated, same as A
FILTER        :: Deprecated, same as A

1-3/ Minitest Spec-Style

Minitest support a spec-style syntax using describe and it blocks instead of classes. Consider the class-style syntax above:

class TheTruthTest < Minitest::Test
  def test_one_plus_one_equals_two
    assert_equal(2, 1+1)
  end
end

Let’s rewrite it using spec-style: Change the class itself to a describe block, putting the class name into quotation marks and removing the base class. Then change def test_one_plus_one_equals_two to it "should add one plus one to make two"

describe "TheTruthTest" do
  it "one plus one equals two" do
    assert_equal(2, 1+1)
  end
end

Under the hood, Minitest converts this into what you see above, making this functionally equivalent.

You cannot mix & match syntax in the same file. To use the it style blocks, they must be in describe blocks.