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.