Ever since I started working with Ruby I have been using RSpec to test my apps and gems without giving minitest much thought. Recently I started a new non-Rails project and decided to give Minitest a try just for the fun of it. Migrating from one tool to another was refreshingly fun due to the fact that that Minitest and RSpec aren’t so different from each other – they both have the basic features that we need in a testing library to get things running, and if you are used to testing your code moving from one to the other might not be so scary as you might expect.

Translating testing idioms

One of the first things that I looked into was how some of common RSpec idioms should be implemented when using Minitest.

The classic ones are fairly simple: the before and after lifecycle hooks should be equivalent as implementing the setup and teardown methods in your test class, and you have control over the inheritance chain by selecting when/where to call super . let and subject can be achieved with methods that use memoization to cache their values.

# A classic RSpec subject/before usage. require 'spec_helper' describe Post do subject(:post) { Post.new } before { post.publish! } end # The equivalent with Minitest & Ruby. require 'test_helper' class PostTest < Minitest::Test def post @post ||= Post.new end def setup post.publish! end end

RSpec shared examples, where you can reuse a set of examples across your test suite, can be replicated by simply writing your tests in modules and depend on accessor methods to inject any objects your tests might depend on

# What used to be a shared_examples 'Serialization' can be a module... module SerializationTests def serializer raise NotImplementedError end end # And your test cases can include that module to copy the tests class JSONSerializationTest < Minitest::Test include SerializationTests def serializer JSON end end class MarshalSerializationTest < Minitest::Test include SerializationTests def serializer Marshal end end

Mocks and stubs, which are incredibly flexible when using RSpec, are available in Minitest without any third party gem:

class PostTest < Minitest::Test def test_notifies_on_publish notifier = Minitest::Mock.new notifier.expect :notify!, true post.publish!(notifier: notifier) notifier.verify end def test_does_not_notifies_on_republish notifier = Minitest::Mock.new post.stub :published?, true do post.publish!(notifier: notifier) notifier.verify end end end

If you want a different or more fluent API, you can use something like mocha to improve your mocks, or even bring RSpec API into the mix – with some manual setup you can pick the rspec-mocks gem and define your mocks and stubs just like when using the complete RSpec tooling:

require 'rspec/mocks' class PostTest < Minitest::Test include ::RSpec::Mocks::ExampleMethods def before_setup ::RSpec::Mocks.setup super end def after_teardown super ::RSpec::Mocks.verify ensure ::RSpec::Mocks.teardown end def test_notifies_on_publish notifier = double('A notifier') expect(notifier).to receive(:notify!) post.publish!(notifier: notifier) end end

Know your assertions

One of my favorite parts of RSpec is how expressive the assertions can be – from the Ruby code that we have to write to the errors that the test runner will emit when something is broken. One might think that we can have something similar when working with Minitest, but that is not exactly true.

Let’s say we want to test a method like Post#active? . Using a dynamic matcher from RSpec like expect(post).to be_active will produce a very straightforward message when that assertion fails: expected #<Post: …>.active? to return false, got true .

With Minitest, we might be tempted to write an assertion like assert !post.active? , but then the failure message wouldn’t be much useful for us: Failed assertion, no message given . But fear not, because for something like this we have the assert_predicate and refute_predicate assertions, and they can produce very straightforward failure messages like Expected #<Post:…> to not be active?. , which clearly explains what went wrong with our tests.

Besides the predicate assertions, we have a few other assertion methods that can useful instead of playing with the plain assert method: assert_includes , assert_same , assert_operator and so on – and every one of those has a refute_ counterpart for negative assertions.

It’s always a matter of checking the documentation – The Minitest::Assertions module explains all the default assertions that you use with Minitest.

And in the case where you want to write a new assertion, you can always mimic how the built-in assertions are written to write your own:

module ActiveModelAssertions def assert_valid(model, msg = nil) msg = message(msg) { "Expected #{model} to be valid, but got errors: #{errors}." } valid = model.valid? errors = model.errors.full_messages.join(', ') assert valid, msg end end class PostTest < Minitest::Test include ActiveModelAssertions def test_post_validations post = Post.new(title: 'The Post') assert_valid post end end

Active Support goodies

If you want some extra sugar in your tests, you can bring some of extensions that Active Support has for Minitest that are available when working with Rails – a more declarative API, some extra assertions, time traveling and anything else that Rails might bring to the table.

require 'active_support' require 'active_support/test_case' require 'minitest/autorun' ActiveSupport.test_order = :random class PostTest < ActiveSupport::TestCase # setup' and teardown' can be blocks, # like RSpec before' and after'. setup do @post = Post.new end # 'test' is a declarative way to define # test methods. test 'deactivating a post' do @post.deactivate! refute_predicate @post, :active? end end

Tweaking the toolchain

Minitest simplicity might not be so great when it comes to the default spec runner and reporter, which lack some of my favorite parts of RSpec – the verbose and colored output, the handful of command line flags or the report on failures that get the command to run a single failure test. But on the good side, even though Minitest does not ship with some of those features by default, there are a great number of gems that can help our test suite to be more verbose and friendly whenever we need to fix a failing test.

For instance, with the minitest-reporters gem you can bring some color to your tests output or make it compatible with RubyMine and TeamCity. You can use reporters that are compatible with JUnit or RubyMine if that’s your thing. You can use minitest-fail-fast to bring the --fail-fast flag from RSpec and exit your test suite as soon as a test fails. Or you can track down object allocations in your tests using minitest-gcstats.

If any of those gems aren’t exactly the setup you want it, you can always mix it up a bit and roll your own gem with reporters, helpers and improvements that are suitable for the way you write your tests.

Thanks to this extensibility, Rails 5 will bring some improvements to how you run the tests in your app to improve the overall testing experience with Rails (be sure to check this Pull Request and the improvements from other Pull Requests).