Test doubles likes mocks and stubs can help us with isolating code under test with the rest of the system. Here’s how to mock in Minitest.
Mocks
Mocks are usually small handcrafted objects that are ready to be passed to methods under test to invoke a specific scenario we want to see in the test.
Let’s imagine a small code example:
class StripeProvider
def charge
# code to charge customer
end
end
class Payment
def initialize(provider: StripeProvider.new)
@provider = provider
end
def charge(user, amount)
@provider.charge(user, amount)
end
end
Now if we would pass a real gateway object in the test, we would have to set up a test environment on the provider side and depend on an HTTP call. Instead we can create a mock payment provider for the test case:
require "minitest/autorun"
class PaymentTest < Minitest::Test
def setup
@mock_provider = Minitest::Mock.new
@mock_provider.expect(:charge, true, [Object, Numberic])
end
def test_user_is_charged
payment = Payment.new(provider: @mock_provider)
assert payment.charge(User.new, 100)
assert_mock @mock_provider
end
end
Here we passed a mocked provider and the code for the Payment
object stayed the same. The expect
method takes three arguments: the method name, return value, and arguments which can be either exact values or types (here any object and number).
Minitest implements mocks in Minitest::Mock
which lets us define expectations for called methods with expect
and then verify them with assert_mock
or verify
. This method confirms the method was actually called.
Since Mocks needs to be passed directly this way in test, they are often used together with stubs. Let’s do one more example, this time we’ll use a stub to return a mock provider automatically:
# test/models/payment_test.rb
require "test_helper"
class PaymentTest < ActiveSupport::TestCase
setup do
@mock_provider = Minitest::Mock.new
def @mock_provider.charge(user, amount); true; end
end
test "#charge" do
StripeProvider.stub :new, @mock_provider do
payment = Payment.new
assert payment.charge(User.new, 100)
end
end
end
As you can see the default StripeProvider
was replaced with our mock. This is a useful technique when passing the mocked object directly is not possible.
Also, we defined charge
without any expectations. We can add methods on the mock object like this with just Ruby when we don’t need to check if they were run.
Get Test Driving Rails while it's in prerelease.