Mocking in Ruby with Minitest
Mocking is used to improve the performance of your tests. This tutorial will show you how to use mocks and stubs in Ruby with Minitest.
In this tutorial, we will cover how to use mocks and stubs in Minitest to improve the performance of your tests and avoid testing dependencies.
To follow this tutorial, you'll need Ruby installed along with Rails. This tutorial was tested using Ruby version 2.3.1, Rails version 5.0, and Minitest version 5.9.1.
Currently, there is no known issue with using earlier or later versions
of any of those, however there will be some differences. Models inherit from
ApplicationRecord, which is the new default in Rails 5.0. We'll also
assert_mock can be used to verify mocks as of Minitest 5.9,
but that will not work with earlier versions where
assert mock.verify was the
method used to verify mocks.
To get started you can use
gem install rails, and you should be good to go,
provided you have Ruby installed.
gem install rails
What is Minitest?
Minitest is a complete testing suite for Ruby, supporting test-driven development (TDD), behavior-driven development (BDD), mocking, and benchmarking. It's small, fast, and it aims to make tests clean and readable.
If you're new to Minitest, then you can take a look at our tutorial on getting started with Minitest.
Minitest is the default testing suite which is included by default with new Rails applications, so no further setting up is required to get it to work. Minitest and RSpec are the two most common testing suites used in Ruby. If you'd like to learn more about RSpec, you can read our tutorial on getting started with RSpec as well as this tutorial on mocking with RSpec: doubles and expectations.
Test Doubles and Terminology
The terminology surrounding mocks and stubs can be a bit confusing. The main terms you might come across are stubs, mocks, doubles, dummies, fakes, and spies.
The umbrella term for all of these is double. A test double is any object used in testing to replace a real object used in production. We'll cover dummies, stubs, and mocks in this tutorial, because they are the ones used commonly in Minitest.
The simplest of these terms is a dummy. It refers to a test double that is
passed in a method call, but never actually used. Much of the time, the purpose
of these is to avoid
ArgumentError in Ruby.
Minitest does not have a feature for dummies, because it isn't really needed.
You can pass in
Object.new (or anything else) as a placeholder.
Stubs are like dummies, except in that they provide canned answers to the methods which are called on them. They return hard coded information in order to reduce test dependencies and avoid time consuming operations.
Mocks are "smart" stubs, their purpose is to verify that some method was called. They are created with some expectations (expected method calls) and can then be verified to ensure those methods were called.
Mocks and Stubs
The easiest way to understand mocks and stubs is by example. Let's set up a Rails project and add some code that we can use mocks and stubs to test.
For this example, we'll create user and subscription models with a subscription service which can be used to create or extend subscriptions.
Assuming you have Ruby and Ruby on Rails set up, we can start by creating our Rails application.
rails new mocking-in-ruby-with-minitest
Now, let's add our user model and tests by using the Rails generator:
rails g model user name:string Running via Spring preloader in process 14377 invoke active_record create db/migrate/20161017213701_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml
Next, let's create the model for subscriptions which has a reference to the user model:
rails g model subscription expires_at:date user:references Running via Spring preloader in process 15028 invoke active_record create db/migrate/20161017214307_create_subscriptions.rb create app/models/subscription.rb invoke test_unit create test/models/subscription_test.rb create test/fixtures/subscriptions.yml
Then, migrate the database:
Finally, let's create a service which creates and manages subscriptions. Start by adding a reference from
class User < ApplicationRecord has_one :subscription end
Now, let's add our subscription service tests. To keep things simple, we don't
test that the
expires_at attribute is always correct.
# test/services/subscription_service_test.rb require 'test_helper' class SubscriptionServiceTest < ActiveSupport::TestCase test '#create_or_extend new subscription' do user = users :no_sub subscription_service = SubscriptionService.new user assert_difference 'Subscription.count' do assert subscription_service.apply end end test '#create_or_extend existing subscription' do user = users :one subscription_service = SubscriptionService.new user assert_no_difference 'Subscription.count' do assert subscription_service.apply end end end
Let's also add a user fixture for the user which has no subscriptions. Add the following two lines to the user fixture file:
no_sub: name: No Subscription
Now, let's make our test pass by adding
# app/services/subscription_service.rb class SubscriptionService SUBSCRIPTION_LENGTH = 1.month def initialize(user) @user = user end def apply if Subscription.exists?(user_id: @user.id) extend_subscription else create_subscription end end private def create_subscription subscription = Subscription.new( user: @user, expires_at: SUBSCRIPTION_LENGTH.from_now ) subscription.save end def extend_subscription subscription = Subscription.find_by user_id: @user.id subscription.expires_at = subscription.expires_at + SUBSCRIPTION_LENGTH subscription.save end end
Now, run the tests to make sure everything is passing.
rake Running via Spring preloader in process 19998 Run options: --seed 23654 # Running: .. Finished in 0.083658s, 23.9069 runs/s, 23.9069 assertions/s. 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
test/services do not exist by default so you will have to create them.
Great! We're now ready to add some functionality, which we can benefit from by using mocks and stubs in the tests.
Stubbing is useful when we want to replace a dependency method which takes a long time to run with another method that has the return value we expect.
However, it's usually not a good idea to do this if the method belongs to the class you are testing, because then you're replacing the method you should be testing with a stub. It's fine to do this for methods of other classes that have their own tests already, but are called from the class we are testing.
Let's add a method to
#apply_subscription. This method will call
SubscriptionService to apply the subscription. In this case, we have already
tested the subscription service, so we don't need to do that again. Instead, we
can just make sure it is called with a combination of stubbing and mocking.
In order to create mocks, we also need to load Minitest in
require call to the ones in
# test/test_helper.rb require 'minitest/autorun'
Now, let's add tests where we use a mock to mock
#apply to just return true without ever calling the real
# test/models/user_test.rb require 'test_helper' class UserTest < ActiveSupport::TestCase test '#apply_subscription' do mock = Minitest::Mock.new def mock.apply; true; end SubscriptionService.stub :new, mock do user = users(:one) assert user.apply_subscription end end end
Since we have already tested
SubscriptionService, we don't need to do it
again. That way, we don't have to worry about the setup and the overhead of
accessing the database, which makes our test faster and simpler.
Now, let's add the code to make the test pass.
# app/models/user.rb class User < ApplicationRecord has_one :subscription def apply_subscription SubscriptionService.new(self).apply end end
Although we have demonstrated how stubbing works here, we are not really testing anything, to do that we need to make full use of mocks.
One of the core functionalities of mocks is to be able to verify that we called a method that we stubbed. Sometimes this isn't something we want to, however a lot of the time, we want to make sure we called some method, but we don't care to test if it works or not, because it's already been tested.
Let's change our test to verify that
called, even though it calls our stub instead of the real thing.
# test/models/user_test.rb require 'test_helper' class UserTest < ActiveSupport::TestCase test '#apply_subscription' do mock = Minitest::Mock.new mock.expect :apply, true SubscriptionService.stub :new, mock do user = users(:one) assert user.apply_subscription end assert_mock mock # New in Minitest 5.9.0 assert mock.verify # Old way of verifying mocks end end
Note how we tell our mock what method call we are expecting along with the return value. It's possible to pass in a third argument, which is an array of arguments that the method is expected to receive. This needs to be included if the method has any arguments passed to it in the method call.
Sometimes we want to be able to change the return value of calling a constant in a class from a test. If you're coming from RSpec, you might be used to having this feature in your toolbelt. However, Minitest doesn't ship with such a feature.
There's a gem which provides this functionality for Minitest called minitest-stub-const.
It can be quite useful when you want to change the value of a constant in your class, e.g when you need to test some numerical limits. One common use is results per page in pagination. If you have 25 results per page set in a constant, it can be easier to stub that constant to return 2, reducing the setup required to test your pagination.
Overusing Mocks or Stubs
It's possible to overuse mocks or stubs, and it's important to be careful and
avoid doing that. For example, if we stubbed the test for
in order to just return some data instead of opening a real file and performing the search
on it, we wouldn't actually know if
This is a rather obvious case. However, there are more subtle scenarios where mocks and stubs should be avoided.
In this tutorial, we have covered how to use mocks and stubs in Minitest. If you followed along, you should now have a Rails application with functioning tests that make use of mocks and stubs. You should now have an idea of the kinds of tasks for which it is beneficial to use mocks and stubs.
If you found this tutorial helpful, you might want to check out some of the other tutorials on testing in the Semaphore Community.
We'd also like to hear your comments and questions, so feel free to leave them in the section below.
P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We've recently published an ebook covering just that — "Rails Testing Handbook". Learn more and download a free copy.