Notes to self

Introduction to Minitest Mocks

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.

Check out my book
Interested in Ruby on Rails default testing stack? Take Minitest and fixtures for a spin with my latest book.

Get Test Driving Rails while it's in prerelease.

by Josef Strzibny
RSS