One of the reasons I prefer testing with Minitest is the dissatisfaction with my everyday RSpec work. Here are things I don’t like about typical RSpec test suites and how to fix them.
Please take the post with a grain of salt. If you enjoy writing RSpec this way, keep enjoying it! Some things are personal, and it’s okay.
Shared examples
I think the shared examples feature deserves the first spot. While I am confident that the idea to DRY your tests comes from a well-intentioned place, the reality is that it’s a confusing part of the test that is hard to debug.
What is a shared example? It’s a reusable generic example like this:
RSpec.shared_examples(:calculator) do
let(:klass) { described_class }
let(:options) { {} }
subject { klass.new(options).calc(expression) }
it { is_expected.to(eq(result)) }
end
Above I defined a test for calculators that takes options
on initialization and passes an expression
to the calc
method. Then checks the result
. Notice how you scan the whole example to know what you have to provide to make the test work.
Once prepared, we can now test any instance of a class with just a few lines of code:
RSpec.describe(MyBrandNewCalc) do
it_behaves_like :calculator do
let(:expression) { "5 + 5" }
let(:result) { 10 }
end
end
I kept the example simple enough to follow if you don’t know how shared examples work, but you can imagine that the shared test can be much bigger and more convoluted.
So what are my troubles with that?
First, is the inability to see where the test failed:
...
rspec ./spec/lib/my_brand_new_calc_spec.rb[1:2:5:1:1:1:1]
Second, is trying to connect and compute values provided in the test and the shared example to see what’s passed in the end.
Third, is figuring out how to debug and rerun a failed test like that.
And it seems like I am not alone in these feelings:
Note you can use RSpec without shared examples, so maybe don’t?
Indirect references
The second on the list is the extraction of subject
, is_expected
and described_class
.
I mean, I get it. We are like scientists in a lab. We have a test subject
that will go through many tests, so we never lose sight of what we are testing… or do we?
RSpec.describe(MyBrandNewCalc) do
let(:options) { {} }
let(:expression) { "" }
subject { described_class.new(options).calc(expression) }
describe "#calc" do
let(:expression) { "5 + 5" }
let(:result) { 10 }
it { is_expected.to(eq(result)) }
end
end
When seeing a small example like this, it might not be immediately apparent what the problems are.
But imagine a long test and arriving somewhere in the middle:
...
describe "#calc" do
let(:expression) { "5 + 5" }
let(:result) { 10 }
it { is_expected.to(eq(result)) }
end
...
What is the subject? What really is referred in is_expected
? Is the subject a class, instance of a class, or expression?
What if the test looked like this?
...
describe "#calc" do
let(:calculator) { MyBrandNewCalc.new }
it "returns 10" do
expect { calculator.calc("5 + 5") }.to(eq(10))
end
end
...
Suddenly I know what’s being run, and things are 100% clear. Just don’t make me think more than necessary.
Note you can use RSpec without this kind of indirection, as I demonstrated.
Conclusion
I use and prefer Minitest precisely because it’s so bare bone. I can think about the domain problems rather than trying to make sense of the test suite. It’s a feature. Minitest is also a Rails default which has other benefits.
I hope to blog more about how I write Minitest, so stay tuned. In the meantime, check out my Rails kit, which uses 100% Minitest.
Get Test Driving Rails and make your tests faster and easier to maintain.