Lazy Eval With Minitest & TestUnit
I’ve been working on a project recently where the requirement was to use MiniTest to spec out an API, with the main rationale being that the new version of MiniTest in Rails 6 runs in parallel, out of the box.
As a staltwart of Rspec, I find RSpec’s lazy evaluation using the let(:x) { :y }
syntax is one of it’s nicest features for writing dry concise tests and is widely used to help keep tests clear and focussed.
Minitest, as far as I can see, doesn’t have this feature when using TestUnit syntax for writing tests, however you can approximate it using Ruby’s built in lambda syntax.
Take this simple example using Rspec’s very nice, high level testing DSL.
let(:num) { 'NaN' }
let(:number_cruncher) { OpenStruct.new(crunch: num) }
describe 'The NumberCruncher factory' do
it 'returns a NumberCruncher' do
expect(number_cruncher.crunch).to eq('NaN')
end
context 'With a Crunch' do
let(:num) { 'Crunch' }
it 'returns a Crunch' do
expect(number_cruncher.crunch).to eq('Crunch')
end
end
end
In MiniTest, I can approximate it with the following.
class NumberCruncherTest < ActiveSupport::TestCase
setup do
num = -> { @num } # variables that we want to change per test are wrapped in a lambda that only needs to be declared locally.
# anything we call directly in a test is assigned as an instance variable so we can access it,
# note that all local lambdas are invoked with '.()' - a shorthand for '.call', inside the @number_cruncher, making use of closures to get that lazy eval effect.
@number_cruncher = -> { OpenStruct.new(crunch: num.()) }
end
test 'The NumberCruncher factory returns a NumberCruncher' do
@num = 'NaN' # the instance variable for the num local variable declared in the setup block
assert_equal('NaN', @number_cruncher.().crunch)
end
class WithACrunch < self
test 'The NumberCruncher factory returns a Crunch' do
@num = 'Crunch'
assert_equal('Crunch', @number_cruncher.().crunch)
end
end
end
Now I can write pretty dry, lazily evalled tests, because I don’t have to re write all that boilerplate setup code again ( A nice feature that really makes Rspec’s DSL great! ) but now I will also get MiniTests new built in parallel execution for free!
Here is the same test without the use of lambda:-
class NumberCruncherTest < ActiveSupport::TestCase
setup do
@num = 'NaN'
@number_cruncher = OpenStruct.new(crunch: @num)
end
test 'The NumberCruncher factory returns a NumberCruncher' do
assert_equal('NaN', @number_cruncher.crunch)
end
class WithACrunch < self
setup do
@num = 'NaN'
@number_cruncher = OpenStruct.new(crunch: @num)
end
test 'The NumberCruncher factory still returns a NaN' do
@num = 'Crunch' # this wont work now since it evals sequentially
assert_equal('NaN', @number_cruncher.crunch)
end
test 'The NumberCruncher factory returns a WangerNum, but only with the boilerplate' do
@num = 'WangerNum' # this will work now since we overwrite @number_cruncher next line
@number_cruncher = OpenStruct.new(crunch: @num)
assert_equal('WangerNum', @number_cruncher.crunch)
end
end
end
So you can see how this is going to get pretty unwieldy in a large test suite, without using a let
like strategy via lambda or Procs.