Not quite a dog, not quite a developer either!

Making MiniTest work like RSpec

mhz
3 min readJan 1, 2020

--

When I switched from Rspec to MiniTest I immediately noticed an unnerving lack of features from MiniTest when building complex test scenarios. Here is a simple guide to mold your MiniTest framework to “quack” a little more like RSpec.

I have provided examples for both vanilla MiniTest as well as with Rails’s ActiveSupport::TestCase that by default uses MiniTest in the background.

Faking the DSL

One of the things I really appreciated with RSpec was how legible the DSL was. It abstracted class definitions, methods and assertions to make it read more like English rather than code.

When I moved over to MiniTest, I really wanted to be able to write tests with the same Spec-styled language. Using it , describe , before , and after blocks.

before(:all), after(:all), and around(:all) blocks

I missed this key detail in the beginning, and for almost a whole year, I was writing tests wrong in my setup. I simply assumed that the Spec DSL from Minitest would also give the *(:all) options to my blocks.

Although I was providing the :all to my blocks, none of them raised an error, or displayed any signs that my attempts were futile.

So one day a co-worker pointed it out, and I put a print statement inside a before(:all) block just to check. It printed for ever one of my tests. It was running before each test, instead of before all the tests.

I felt pretty silly.

I didn’t look that cute

You actually need minitest/hooks library to get that functionality. Here is how to set it up.

Setting up all of the above, but in Rails

Its actually exactly the same, except I prefer to monkey-patch ActiveSupport::TestCase instead of creating a child class from that. It makes the extension invisible to other users of the code — just a convenience.

Of course, the minitest-hooks gem should be defined only for your test environment in the Gemfile, and requiring minitest/hooks can be in your test_helper.rb for example. I have noticed that I must explicitly include Minitest::Hooks in my test classes, and simply defining require 'minitest/hooks/default' doesn’t work.

Some caveats of the :all hooks are:

  • They execute before any of the other before blocks, and after the after blocks.
  • Fixtures don’t get loaded at that stage. you have to manually call setup_fixtures in the before(:all) block, and teardown_fixtures in the after(:all) block.
  • Models in the DB created in the before(:all) block don’t get rolled back automatically as in the other tests. You have to manually destroy them back in after(:all)
  • If you don’t destroy what you created, you might get lingering entries in your test DB, which would mess with your subsequent test runs.
  • A simple work-around for that is to put a transaction in the around(:all) block, which executes both before(:all) and around(:all) within it. This way, you don’t need to remember to destroy all of the things you created (because there may be a lot of them, chained up from a factory script, or whatever)

In Conclusion

Minitest is fast, RSpec is slow. But you can get the best of both words by doing a little customization to your test setup.

The point is not to re-implement RSpec in Minitest. Remember that you should take advantage of the light-weight of Minitest, and be weary of fattening that up with too much “RSpectedness”.

Even with the test customization, hooks, setup, etc, try to keep it simple. If you are wrestling with the tests too much, consider mocking or stubbing instead, or maybe even refactoring your code.

--

--

mhz

Software Engineer. Using this platform to share knowledge on software, and reflections in life.