Mocks & Spies in Minitest

For one of our smaller ruby projects we didn’t want to bring in all of rspec, so we decided to go with minitest as our unit test runner and assertion library. Doing mocks and spies in minitest proved a little bit challenging to get the hang of initially but it’s actually quite easy. This article will explain how to do it.

First, let’s consider two classes - one helper class which we will be mocking out, and the other is the class under test.

class MyHelperClass
  def upcase_instance(value)
    # value.upcase
    raise "Fail the test if the real thing is called"
  end

  def self.upcase_class_method(value)
    # value.upcase
    raise "Fail the test if the real thing is called"
  end
end

class MyClassUnderTest
  def use_upcase_instance(value)
    helper_class = MyHelperClass.new
    helper_class.upcase_instance(value)
  end

  def use_upcase_class_method(value)
    MyHelperClass.upcase_class_method(value)
  end
end

Note that if the real helper class’s methods are called the test will fail to illustrate that we’re doing the correct thing. In your test file you wil need to bring in the necessary requires for minitest:

require "minitest/autorun"
require "minitest/mock"

The core aspect of mocking in minitest comes from Minitest::Mock’s expect method coupled with the stub method on whatever you’re mocking. The expect method signature looks like this:

expect(name, retval, args = [], &blk)  Object
# Expect that method name is called, optionally with args or a blk, and returns retval.

So we can mock out an instance method like so:

mock = Minitest::Mock.new # Create a new mock
mock.expect(:upcase_instance, "FOURTYTWO", ["FoUrTyTwO"]) # Expect the upcase_instance method to be called with one argument "FoUrTyTwO" and force it to return the value "FOURTYTWO"
MyHelperClass.stub(:new, mock) do # Swap out the real implementation of the "new" method of MyHelperClass to return our mock
  MyClassUnderTest.new.use_upcase_instance("FoUrTyTwO") # Use the method. The expectation has already been set in the previous line
end

There is an additional library you can add called bogdanvlviv/minitest-mock_expectations which can make this a lot simpler if you find yourself writing a lot of mocks and spy assertions. With it we can rewrite the above as

assert_called_on_instance_of(MyHelperClass, :upcase_instance, ["FoUrTyTwO"], returns: "FOURTYTWO") do
  MyClassUnderTest.new.use_upcase_instance("FoUrTyTwO")
end

And for a class method you write

assert_called(MyHelperClass, :upcase_class_method, ["FoUrTyTwO"], returns: "FOURTYTWO") do
  MyClassUnderTest.new.use_upcase_class_method("FoUrTyTwO")
end

That’s it!

Written on August 5, 2020 by podrezo