Getting Started With Minitest
- What is Minitest?
- How to start testing with Minitest
- How to write tests using Spec
- Rake Task
- Finish the test suite
- In Deeper with Minitest
- Benchmarking Minitest
- Minitest & Rails
- References
What is Minitest?
Minitest is a complete, lightweight suite of testing utilities for Ruby.
It was created and is maintained by the Seattle Ruby community, and was accepted into Ruby core for 1.9.
Minitest provides support for TDD, BDD, Mocks/Stubs, Benchmarking, can run Capybara specs, has a spec DSL, can be easily extended however you see fit (it’s Ruby after all).
Here is a quick example of a test in Minitest
require 'minitest/autorun'
class Coffee
attr_reader :color
def initialize
@color = :black
end
end
class CoffeeTest < Minitest::Test
def test_color
fresh_coffee = Coffee.new
assert cup_of_coffee.color == :black
end
end
How to start testing with Minitest
Getting started with Minitest is amazingly easy. If you can write Ruby (even if you’ve only written a few methods) you can write Minitest.
Begin by creating your test file
To get started, create a file called coffee_test.rb
It is recommended to follow the Minitest convention by ending the file name with _test.rb. This is not a hard requirement, but will make for a good habit when adding tests for Rails or other projects that have tooling around _test
.
Next, add minitest/autorun
as a requirement Then create a new class CoffeeTest
as a subclass of Minitest::Test
# coffee_test.rb
require 'minitest/autorun'
class CoffeeTest < Minitest::Test
end
You can add more gems, and utilities to add even more power to Minitest, but keep it simple to start.
As your test suite grows, it’s a good idea to move all the require
s and setup into common place like test_helper.rb
. That way you keep your tests DRY
Write your first test
Now that the boilerplate ready, add the first test method.
getting-started-with-minitest tests are just plain old Ruby methods.
Start by adding a new method starting with test_
to the class.
# coffee_test.rb
require 'minitest/autorun'
class CoffeeTest < Minitest::Test
def test_coffee
end
end
The test_
prefix for the method is how Minitest distinguishes Ruby test methods and other methods in the test file, (like helper methods that handle setup, or create data for use across multiple tests).
_test_
is one way to write tests in Minitest. Another way is to use a Spec DSL, which is explained later in this post._
Write an assertion
An assertion will check to see if the returned value of a block or call evaluates to true
. If so, the test will pass. If not false, the test fails.
To test the opposite, blocks or calls returning false, use refute
. The default Minitest assert
methods have opposite refute
methods that take the same params.
Now write an assertion, in this case, we’re going to test that a new instance of Coffee
is still a Coffee
.
# coffee_test.rb
require 'minitest/autorun'
class CoffeeTest < Minitest::Test
def test_coffee
assert_instance_of Coffee, Coffee.new
end
end
This is done using the assertion assert_instance_of
, which takes a class
, and an obj
, checking whether or not the obj
is an instance of class
.
Running your first test
Running a test file is the same as any Ruby script.
Start up a Terminal in the same location as your test file, and run the following command.
$ ruby coffee_test.rb
The output should be similar to the following.
Run options: --seed 35012
# Running:
E
Finished in 0.000729s, 1371.7422 runs/s, 0.0000 assertions/s.
1) Error:
CoffeeTest#test_coffee:
NameError: uninitialized constant CoffeeTest::Coffee
Did you mean? CoffeeTest
test_001b.rb:5:in `test_coffee'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
Great! Sort of, the test ran successfully, but the it failed.
Fix the failing test case
Looking over the output from the test run, notice the following:
Run options: --seed 35012
This is the seed test was run with. It is randomized each time the test is run. More on seeds below.- A series of
...
s andF
s. These signify successes and exceptions. For now it is just a singleE
- How long the testing took, how many times it will run in per second, and how many assertions we can run per second. This is useful later in determining how fast your suite is. Fast tests = faster development
- List of errors, where they occurred and possible fix
- General stats about the run of tests just performed
The test ran quickly, but failed. Look over the error. It indicates that Coffee
is not defined anywhere. That’s an easy fix.
Add the Coffee
class.
# coffee.rb
class Coffee
def initialize; end
end
# coffee.rb
require 'minitest/autorun'
require_relative 'coffee'
class CoffeeTest < Minitest::Test
def test_coffee
assert_instance_of Coffee, Coffee.new
end
end
And now, run the test again. Same command as before
$ ruby coffee_test.rb
Run options: --seed 5584
# Running:
.
Finished in 0.000671s, 1490.3130 runs/s, 1490.3130 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
The test now passes.
That’s all there is to testing using Minitest.
Add another test case
Add a test that will check an instance of Coffee, will return a list of flavours.
The assertion to use here will be assert_includes
. Which takes a collection
and an obj
, validating that the obj is in the collection.
# coffee.rb
class Coffee
attr_reader :flavours
def initialize
@flavours = %w[Chocolate Honey Toffee]
end
end
# coffee_test.rb
require 'minitest/autorun'
require_relative 'coffee'
class CoffeeTest < Minitest::Test
def test_coffee
assert_instance_of Coffee, Coffee.new
end
def test_coffee_flavour
possible_flavour = 'honey'
tasted_flavours = Coffee.new.flavours.map(&:downcase)
assert_includes tasted_flavours, possible_flavour
end
end
How to write tests using Spec
Minitest comes with a Spec DSL. Which, instead of writing methods, they’re written plain english. If you’re familiar with RSpec, you’ll feel at home with Minitest/Spec.
Write your first spec
Continuing with our previous coffee example, we’ll do the following:
- Update each test
test_description
to usedescribe
- update each test to use
it
statements - Make use of
let
statements
Update test descriptions
We’ll start by moving our class Coffee
above the tests.
Make sure your test follows the class you’re resting. This wasn’t a requirement before because we were creating a new class. In this case, we’re referencing an existing class.
Next, we’ll replace our class CoffeeTest
with a describe block.
# coffee.rb
class Coffee
...
end
# coffee_test.rb
require 'minitest/autorun'
describe Coffee do
...
end
The big change here is
class CoffeeTest < Minitest::Test
becomes
describe Coffee do
Everything else stays the same.
You can nest describe blocks which can help with organizing larger test files into logical pieces.
We’re now ready to continue upgrading our tests to specs.
Replace test methods with it statements
Next, we’ll replace test_
methods with it
statements. This will make our tests read more like plain language, and less like a Ruby method.
# coffee.rb
class Coffee
...
end
# coffee_test.rb
require 'minitest/autorun'
describe Coffee do
it 'gives us a new Coffee' do
...
end
it 'has a honey flavour' do
...
end
end
it
statements define an expectation with a plain english name. If no name is passed in, it will default to anonymous. For readability, it’s recommended to add a name to all it
statements.
Under the hood, an it
statement gets converted into a test_the_thing
method like we had previously.
Replace variable definitions with let
statements
let
allows you to write a concise accessor that memoizes its contents after the first call to it.
For our existing tests, we don’t have anything that needs it. We’re going to make our possible flavours more complicated than just a string so we can try using let
# coffee.rb
class Coffee
attr_reader :flavours
def initialize
@flavours = %w[Chocolate Honey Toffee]
end
end
# coffee_test.rb
require 'minitest/autorun'
require_relative 'coffee'
describe Coffee do
it 'gives us a new Coffee' do
assert_instance_of Coffee, Coffee.new
end
let(:possible_flavour) { %w[chocolate honey toffee].sample }
it 'has a honey flavour' do
tasted_flavours = Coffee.new.flavours.map(&:downcase)
assert_includes tasted_flavours, possible_flavour
end
end
You’ll notice that we defined the let
statement outside of the it
block. This is for reuse in other tests. And because it’s memoized, every other test that calls it will use the same value that was returned the first time it was called.
Another common pattern is to put shared let
statements at the top or at least above a series of tests that depend on them.
You can only access a let
value from inside an it
statement. If you try to access it from outside a it
statement, the test suite will fail.
Replace class calls with subject.
Subject is a lazy man’s generator. It will return the block you specify as subject
.
# coffee.rb
class Coffee
attr_reader :flavours
def initialize
@flavours = %w[Chocolate Honey Toffee]
end
end
# coffee_test.rb
require 'minitest/autorun'
require_relative 'coffee'
describe Coffee do
let(:possible_flavour) { %w[chocolate honey toffee].sample }
subject { Coffee.new }
it 'gives us a new Coffee' do
assert_instance_of Coffee, subject
end
it 'has a honey flavour' do
assert_includes subject.flavours.map(&:downcase), possible_flavour
end
end
You’ll notice that we’ve placed subject
above both of our test cases, it’s also not overly complex. But allows us to not write Coffee.new
all over the place.
Rake Task
Don’t know what rake is? Rake is a task runner for Ruby. This post won’t show you how to use or write Rake Tasks, but how what a simple task looks like for testing. If you want to learn more about Rake, I recommend Stuart Ellis’s - Using Rake to Automate Tasks.
Add Rake to your Gemfile
gem install rake
Add the following to rakefile.rb
.
# rakefile.rb
require 'rake/testtask'
desc 'Run test suite'
Rake::TestTask.new() do |t|
t.pattern = './**/*_test.rb'
end
This will find all files ending with _test.rb
, and then call each using Rake::TestTask
.
rake test
Which will output results that looks exactly it you called the test directly. The difference is, the rake task groups all the tests to gether.
Run options: --seed 52969
# Running:
..
Finished in 0.000582s, 3435.1925 runs/s, 5152.7888 assertions/s.
2 runs, 3 assertions, 0 failures, 0 errors, 0 skips
Finish the test suite
You should now have enough information to start implenting coverage on any of your Ruby programs.
In Deeper with Minitest
Now we’ll take a look into more of the features available in Minitest.
Built in assertions
Minitest comes with a ton of assertions built in, which will cover almost every test case you can think of. We’ll go over a few of the more common assertions below.
Minitest assertions always check for a postive result. If you want to test for negative / false results, each assertion has a negative-twin refute
. Which take the same params as their positive sibling, but make sure the result is the opposite.
assert(predicate, msg = nil)
fails unless predicate
returns true
require_relative 'test_helper'
describe Coffee do
it { assert(Coffee.new.rating == 'Amazing!') }
end
assert_empty(obj, msg = nil)
fails unless obj
is empty
require_relative 'test_helper'
describe Coffee do
it { assert_empty Coffee.new.extras }
end
assert_includes(collection, obj, msg = nil)
fails unless collection
includes obj
require_relative 'test_helper'
describe Coffee do
subject { Coffee.new.flavours }
it { assert_includes subject.map(&:downcase), $possible_flavour }
end
assert_match(matcher, obj, message = nil)
fails unless matcher
=~ obj
require_relative 'test_helper'
describe Coffee do
matcher = /[a-zA-Z].+\!/
it { assert_match matcher, Coffee.new.rating }
end
assert_nil(obj, msg = nil)
fails unless obj
is nil
require_relative 'test_helper'
describe Cup do
it { assert_nil Cup.new.filling }
end
assert_raise(*exp)
fails unless the block raises an exception
require_relative 'test_helper'
describe 'Cup' do
let(:coffee) { Coffee.new }
let(:cup) { Cup.new }
it 'raises when we add another cup' do
assert_raises RuntimeError do
cup.add(Cup.new)
end
end
end
Minitest comes with a wide range of assertions for testing. You can see all of the available assertions and refutions in the Ruby Docs - Assertions
Using Seeds to replay tests
If you’ve watched the output as you’ve run throug Minispec, you’ll probably have noticed the following
Run options: --seed 59802
A seed is a representation of the order of tests, as well as the randomized data inside your tests. Keeping track of seeds allows you, or someone else to rerun the test exactly as a previous time. This helps for debugging failures.
To specify seeds you need to pass in the value when running your test suite.
If you run tests using the Ruby CLI
ruby test -s 59802
If you run tests using a Rake Task
rake test SEED=59802
Setting up and tearing down tests
As you start building out tests for your code. You’ll notice patterns of setup, start happening around your tests.
Minitest has you covered for wrapping each test with setup code, using setup
and teardown
require_relative 'test_helper'
class CoffeeTest < Minitest::Test
def setup
@cup = Cup.new
@cup.add(Coffee.new)
end
def teardown
@cup.drink
end
def test_cup_is_not_empty
refute @cup.empty?
end
end
If you’re using specs, you can also use before
and after
require_relative 'test_helper'
describe 'Coffee' do
let(:cup) { Cup.new }
before do
cup.add(Coffee.new)
end
after { cup.drink }
it 'checks the cup has coffee in it' do
refute cup.empty?
end
end
setup
, teardown
, before
, and after
will be run around each test. This can add significant delays to your tests if you’re putting complex setup logic in the setup code. You can add the gem minitest-hooks
to get access to before_all
, after_all
which runs around all tests in the test file. This is more commonly used when testing many parts of an instance that has a decent amount of setup
Skipping tests
There comes a time when you are testing your code, and you realize you need to test something else as well. You can remember what you want to write, you can leave yourself a comment, or you can write a skip
statement.
Using skip
is the same as using any of the assert
, refute
methods.
require_relative 'test_helper'
describe 'Coffee' do
it 'checks the coffee rating' do
assert(Coffee.new.rating == 'Amazing!')
end
it 'makes sure the taste is not bitter' do
skip 'coffee.taste should not be bitter'
end
end
Skip statements have an advantage over leaving a comment. That is the test reporter will show a list of all the skipped tests, so if you forget one, there is a small indicator to let you know you still have some work to do.
But beware if you use setup/teardown, or before/after to do complex setups, these will still be run around the skipped test. This can increase your test time.
Stubbing
Like other testing libraries, stub
s temporarily replace a method and return the specified result. The original method is then swapped back in when the stub block is finished.
Stubs are great for temporarily replacing methods that may take too long to run in your test suite, or are incapable of running in the environment. This does not mean that method should never be tested. Just that the method might be better tested elsewhere.
class Cup
...
def slow_drink
sleep 10
puts '*slurp*'
sleep 10
puts '*slurp*'
sleep 10
@filling = nil
end
...
end
require_relative 'test_helper'
describe 'Cup' do
let(:coffee) { Coffee.new }
let(:cup) { Cup.new }
before { cup.add coffee }
it 'drinks a coffee slowly' do
cup.stub :slow_drink, cup.filling = nil do
cup.slow_drink
assert_nil cup.filling
end
end
end
Unlike a Mock
the method needs to exist prior to stubbing. You can’t use a stub on a method that does not exist.
If you must test a method that doesn’t exist, you can use a singleton method to create a new non-existant method
Benchmarking Minitest
Minitest comes with benchmarking baked in. This is handy for testing out performance for the code you write.
There are 2 posts from Chris Kottom that describe Benchmarking with Minitest from a high level, as well as practical examples. I recommend reading them a few times.
Minitest::Benchmark: An Introduction
Minitest::Benchmark: A Practical Example
Minitest & Rails
Rails by default uses Minitest to run your tests. In fact, Rails itself is tested using minitest.
The default Rails test_helper.rb
is configured to run test
style tests, it looks like the following.
# test_helper.rb
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
fixtures :all
# Add more helper methods to be used by all tests here...
end
We can add support for Minitest::Spec DSL pretty easily.
# test_helper.rb
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'minitest/autorub'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
fixtures :all
# Add more helper methods to be used by all tests here...
extend MiniTest::Spec::DSL
register_spec_type(self) do |desc|
desc < ActiveRecord::Base if desc.is_a?(Class)
end
end
With spec support inside test_helper.rb
you can now write test, specs, and any combination of features to test your Rails code. MAGIC
References
This post only covers a small portion of what Minitest can do. That is not including what the numerous extensions bring to the table.
The following links should help you on your way to mastering Minitest.