Mocking and Stubbing with the Minitest Framework

Gil Roman
Gil Roman
Sep 7, 2018 · 3 min read

When learning TDD one often hears the terms: stub, mock, dummy and isolated unit testing. The goal of this tutorial is to explain these terms and illustrate their implementation using Ruby’s Minitest framework. This tutorial is influenced in part by Ilija Eftimov’s excellent blog post : https://ieftimov.com/test-doubles-theory-minitest-rspec and Gary Bernhardt’s talk on boundaries https://www.destroyallsoftware.com/talks/boundaries

Test Isolation

Test isolation refers to the removal of dependencies from our tests and only running the production code that is being tested. In order to achieve this, we simulate any dependencies within our tests with dummy objects, stubs or mocks depending on the requirements of the code being tested.

Dummies

Dummy objects are used whenever our tests requires an object but doesn’t expect to use any behavior from the object. In MiniTest the simple way to implement this is by using a default Ruby object.

In the example below a customer dummy object is used because the Order class requires it in order to be initialized, but ‘add_item’ doesn’t call any of the customer object’s methods.

require 'minitest/autorun'describe Orders do
it 'adds an item to an order' do
customer = Object.new
order = Order.new(customer)
item = Item.new
order.add_item(item) assert_includes(order.items, item)
end
end

Stubs

Stubs are used when the function being tested requires a return value or functionality from the dependency. As a simple example, the order class has a ‘place_order’ method that calls the ‘form_of_payment_on_file?’ method on the Customer object and expects a value of true before labeling the order as placed.

class Order
attr_accessor :placed
def initialize(customer)
@customer = customer
end
def place_order
if @customer.form_of_payment_on_file?
@placed = true
end
end
end

In the test we create a mock customer object to pass as an argument to the initialize method of the Order class. For stubs to work with Minitest the method being stubbed needs to exist in the object already. Hence, we are creating a MockCustomer class in our test with the ‘form_of_payment_on_file?’ method defined.

To create the stub we call the stub method on the object. The stub method accepts a method name, a return value and a block as arguments.

require 'minitest/autorun'class MockCustomer
def form_of_payment_on_file?
end
end
describe Order do
it 'places an order' do
customer = MockCustomer.new
order = Order.new(customer)
customer.stub :form_of_payment_on_file?, true do
order.place_order
assert order.placed
end
end
end

Mocks

Martin Fowler on his blog defines mocks as follows:

Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don’t expect and are checked during verification to ensure they got all the calls they were expecting.

A mock is very similar to a stub, but adds verification that the methods being mocked have been called and the test passing is contingent upon this verification. For simplicity let’s convert our example above into a mock.

In Minitest we will initialize a mock customer object with the command ‘Minitest::Mock.new’. On the mock object the ‘.expect’ method is called to assign the method name and the return value that the production code is expected to call. They are passed as two arguments. The ‘.verify’ method is how the test verifies that the object received calls to the methods expected and it’s return value determines if the test passes or fails.

require 'minitest/autorun'describe Order do
it 'places an order' do
customer = Minitest::Mock.new
order = Order.new(customer)
customer.expect :form_of_payment_on_file?, true

order.place_order

customer.verify
end
end

Benefits of Test Isolation

Gary Bernhardt on his talk mentions these benefits of test isolation:

  1. Enhanced TDD by exposing design flaws, if the tests are requiring many nested mock objects.
  2. We can build parts of the system that rely on parts of the system that haven’t been built yet.
  3. The test suite will run faster since it doesn’t have to rely on the dependencies to be instantiated.

Downside of Test Isolation

There is a big downside to test isolation, and that is that the mocks on the test suite can fall out of sync with the production code, and possibly result in a situation where tests pass but the system fails.

In summary, the Minitest framework has a simple syntax to allow for the creation of dummies, stubs, and mocks inside our tests. These terms refers to objects of varying functionality. As with most tools, there are benefits and tradeoffs to test isolation.

Gil Roman

Written by

Gil Roman

Software engineer exploring the boundary between art and technology.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade