Minitest is good for mocking, right? Well… Link to heading
Minitest is gaining a lot of popularity and can actually be a 100% replacement for RSpec. It’s a pure ruby testing framework, it’s fast, light weight, and it supports both a test-unit like syntax and a spec engine with Rspec like syntax.
Still, when it comes to mocking, it can be a little painful. You have to initialize mocks and verify them manually after running the code under test.
A typical unit test with mocks looks something like this:
test 'a pause can be completed' do
datetime_service = MiniTest::Mock.new
pause = MiniTest::Mock.new
success = MiniTest::Mock.new
failure = MiniTest::Mock.new
now = DateTime.now
datetime_service.expect(:datetime_now, now)
pause.expect(:completed_at, nil)
pause.expect(:update_attributes, true, [completed_at: now])
success.expect(:call, true, ["Pause completed", pause])
UseCases::Pauses::Complete.new.run(pause, datetime_service, success, failure)
datetime_service.verify
failure.verify
success.verify
pause.verify
end
What I don’t like in the code above is the verbosity in the setup (and verification) of mock objects. I’m relying quite heavily on mocks, as I don’t want to pass real objects to my unit tests, and this kind of repetition is not good.
Also I want to have a way to distinguish mock objects from “real” objects. This could help seeing if there is too much “real” stuff inside the test, or if I’m correctly mocking all the dependencies and collaborators of the method under test.
What I came up with is Mocked
, a small module to streamline these operations.
# test/utils/mocked.rb
module Mocked
def add_mocks(*names)
names.each { |name| add_mock(name) }
end
def add_mock(name)
@mocks[name] = MiniTest::Mock.new
end
def mocked(name)
@mocks[name]
end
def verify_mocks
@mocks.each do |_, mock|
mock.verify
end
end
def setup_mocks
@mocks = Hash.new
end
end
# test/test_helper.rb
#...
require 'utils/mocked'
# ...
class ActiveSupport::TestCase
include Mocked
setup :setup_mocks
teardown :verify_mocks
end
We are just keeping an hash of mock objects, and verifying them on teardown of the test; we are also giving a mocked(mock_name)
accessor to retrieve mock objects.
With this we can rewrite the test above like this:
test 'a pause can be completed' do
add_mocks(:datetime_service, :pause, :success, :failure)
now = DateTime.now
mocked(:datetime_service).expect(:datetime_now, now)
mocked(:pause).expect(:completed_at, nil)
mocked(:pause).expect(:update_attributes, true, [completed_at: now])
mocked(:success).expect(:call, true, ["Pause completed", mocked(:pause)])
UseCases::Pauses::Complete.new.run(
mocked(:pause),
mocked(:datetime_service),
mocked(:success),
mocked(:failure)
)
end
The code looks better IMHO, and I like that if I decided - i.e. - to replace the pause
mock with a real object it would read like:
test 'a pause can be completed' do
add_mocks(:datetime_service, :success, :failure)
now = DateTime.now
mocked(:datetime_service).expect(:datetime_now, now)
pause = FactoryGirl.create(:pause, completed_at: nil)
mocked(:success).expect(:call, true, ["Pause completed", pause])
UseCases::Pauses::Complete.new.run(
pause,
mocked(:datetime_service),
mocked(:success),
mocked(:failure)
)
assert_equal now, pause.completed_at
end
Here you can clearly see at a first glance that the only real object is pause
, whereas other objects are all mocked. It also really helps when refactoring tests.
What do you think about this? Would you like to be built into a gem, do you have any suggestions or criticism on this? Let me know and have a great day!