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.
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 theafter
blocks. - Fixtures don’t get loaded at that stage. you have to manually call
setup_fixtures
in thebefore(:all)
block, andteardown_fixtures
in theafter(: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 inafter(: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 bothbefore(:all)
andaround(: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.