Stubbing and Mocking in MiniTest
When I started using MiniTest one of the things I struggled with was how to mock and stub objects when constructing my tests. Here are a few patterns I’ve collected over time and now find helpful when stubbing and mocking in MiniTest.
Basic MiniTest Stub
MiniTest comes with a way to stub a value for a test. Here is a classic example stubbing time:
Time.stub :now, Time.new(2012, 11, 14).utc do
MyObject.new.are_we_there_yet?
end
In this example, the :now
method on the Time
object is stubbed so that it
always returns the same time while testing MyObject
.
#stub
takes a few arguments, the first is the method you wish to stub and the
second is the value it should return when it’s called. This second value can
also be an object that responds to #call
and if it does the return value is
the result of calling the object. Which leads us to a second pattern.
Turn Your Stub Into a Spy
Since we can supply an object that responds to call for our stub, let’s turn our stub into a “spy”. Spies not only return a canned answer but are concerned with how they are called. To do this, we can use a Ruby lambda. Inside of our lambda we’ll make some assertions about the arguments the stubbed method is called with.
test "Disabling Alerts" do
command = AlertsPause.new(user)
response = nil
args_checker = lambda do |args|
assert_equal "1234qwfp", args[:user_id]
assert_equal "team1234", args[:group_id]
end
Connection.stub :disable_notifications, args_checker do
response = command.response
end
assert_match(/paused alerts/, response[:text])
end
In this example we are using a lambda to ensure that disable_notifications
gets called with the expected arguments. When the disable_notifications
method
is called on Connection
, args_checker
will be called with the arguments
passed to that method. Inside the lambda we use assertions to “spy” on the
caller of this method and make sure it passes us the arguments we are expecting.
One other thing to note here, if you are using this approach and want to make
assertions on the result of something called within the stub
block, you have
to declare the local variable before the block for it to be available in the
scope outside of the block. In this example response
is used in this way; it
is declared and set to nil
at the beginning of the spec, assigned in the
block, and the used in an assertion at the end.
MiniTest::Mock
MiniTest::Mocks are object doubles you can use as a stand-in for some other object. Their use is really simple:
status = MiniTest::Mock.new
status.expect :fine?, true
checker = EverythingsFineChecker.new(status)
checker.is_everything_fine?
# => true
status.verify
We can use #expect
to stub methods and set return values on our mock object.
If we want it to spy for us, we can call verify
at the end to make sure that
the stub was actually called. The first argument of expect is the method to
stub, the second is the value to return, and the third is optional and can be an
array of arguments. If we want to stub a method set_speed
that accepts an
argument of speed, we might do it like this for a speed of 55:
car = MiniTest::Mock.new
car.expect :set_speed, car, [55]
Return a MiniTest::Mock from a Stub
You can combine stubs and mocks! In this example, a client is stubbed so that
it returns a mock client when it’s initialized. The mock_client
is stubbing a
call to list_all
to always return an empty array.
mock_client = MiniTest::Mock.new
mock_client.expect :list_all, []
Client::Request.stub :new, mock_client do
# do something using the client
end
Using Standard Ruby Objects as Test Doubles
We can also use plain old Ruby in our test! One example of this is using a test
object created in the test as a stand-in for something else. Here we create a
mock client object that responds to list
with some pre-canned answers and then
use dependency injection to pass it into our job instead of one that might make
real requests to an API
class ProcessingTest < MiniTest::Unit::TestCase
class MockClient
def list(page:)
case page
when 1
["item 1", "item 2"]
when 2
["item 3", "item 4"]
else
[]
end
end
end
test "Process Items" do
ProcessingJob.new(client: MockClient.new).perform
# assert something
end
end
Redefine Methods on Ruby Objects
Here is a cool trick you might remember from another one of my recent
posts,
using plain Ruby you can redefine a method on any object. We can use that in a
test to stub methods! Here is an example of stubbing some methods on an instance
of team
, one of which raises an exception.
team = Team.create(name: "Engineering")
def team.active?; true; end
def team.add_member(*args); raise TeamError::NotAuthorized; end
TeamFinder.stub :engineering_team, team do
# Do something
end
# assert something
In that example, team is stubbed so that it always returned true
for active?
and so that it will raise an exception when calling add_member
.
The syntax is a little weird at first, but you’re likely familiar with defining class methods like:
def self.say_hello
puts "hello"
end
This is the same thing! self
in this case refers to the instance of class
representing the object and you are defining the “say_hello” method on it.
Using this technique is pretty powerful because it lets you quickly stub methods on any object and is especially handy when you have several methods to stub. The cool part is, it’s just Ruby!
Use The Tests, Luke
There are some patterns I’ve found helpful when writing tests in MiniTest. If you find that you are having to stub and mock a lot or that you are stubbing things to return other stubbed things that return other stubbed things it could mean that your class knows too much about other objects in your system. I’ve found it helpful to use this pain when I encounter it as an indicator that I should re-consider some closely coupled objects and find ways to make them less coupled.