Mocking and Stubbing with the Minitest Framework
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
enddescribe 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:
- Enhanced TDD by exposing design flaws, if the tests are requiring many nested mock objects.
- We can build parts of the system that rely on parts of the system that haven’t been built yet.
- 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.
