Mocking in ruby with minitest

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.

Cut your Rails test suite down to a few minutes with one-click automatic parallelization.

Automate parallelizing tests

Introduction

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.

Prerequisites

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 ActiveRecord::Base instead of ApplicationRecord, which is the new default in Rails 5.0. We'll also demonstrate that 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.

Dummies

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

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

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:

rake db:migrate

Finally, let's create a service which creates and manages subscriptions. Start by adding a reference from User to Subscription.

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 SubscriptionService.

# 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

Note that app/services and 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

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 User called #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 test_helper.rb. Add this require call to the ones in test_helper.rb:

# test/test_helper.rb

require 'minitest/autorun'

Now, let's add tests where we use a mock to mock SubscriptionService and stub #apply to just return true without ever calling the real SubscriptionService.

# 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.

Mocking

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 SubscriptionService#apply was 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.

Stubbing Constants

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 SubscriptionService 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 SubscriptionService works.

This is a rather obvious case. However, there are more subtle scenarios where mocks and stubs should be avoided.

Conclusion

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. Semaphore is working on a book "The Ultimate Guide to BDD with Rails". Sign up to receive a FREE copy.

51cee7009c304faca416b4475bd42446
Heidar Bernhardsson

I'm a London based Ruby consultant. Visit my website for more information.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.