DEV Community

Oinak
Oinak

Posted on

Mocks and stubs in ruby unit tests, a cheatsheet

Based on Sandi Metz's very precise explanations, I will try to give you the ultimate mnemonic (if you learn it) or cheatsheet (if you bookmark/copy it) to the question what to test?.

First, let's establish the terms I will use to describe the four options:

Query: is a method that returns data but does not change the state of the receiver object, i.e.:

>> a = [1,2,3]
=> [1, 2, 3] 
a.size
=> 3 
>> a
=> [1, 2, 3] 
Enter fullscreen mode Exit fullscreen mode

Command: is a method that may or may not return a value, but it always changes the state of the receiver object.

>> a = [1,2,3]
=> [1, 2, 3] 
>> a.pop
=> 3 
>> a
=> [1, 2] 
Enter fullscreen mode Exit fullscreen mode

Incoming: is a method defined in the class you are testing, and which behaviour you want to test on its unit test.

Outgoing: is a method defined out of the class you are testing, and which behaviour you need to simulate but don't want to test on your unit test.

Ok, those are the definitions, now for the rules:

1. Incoming query:

  • Assert the return value of the method

2. Incoming command

  • Assert the return value of the method (if there is one)
  • Assert the change in state

You changed state might not be accesible via the public interface of your object, but as Xavier Noria says, if it's important enough to need a text, then it's worth using .send() or instance_variable_get() to check on it. Take advantage of ruby's advanced introspection capabilities.

3. Outgoing query

  • Stub the response of the other class

The gist here is, you need some value provided by some external class but that class does not mind if you call it or not, as it states remains unchanged.

So, for your test, what you need is that value, and not the class that provides it, especially if it's not-yours, complicated to set up for answering a particular value, not under your control, etc.

This is how you stub a method call with Minitest's included stub system:

  OtherKass.stub :query, "response" do
    assert_equal "expected_value", obj_under_test.tested_method
  end
Enter fullscreen mode Exit fullscreen mode

4. Outgoing command

  • Stub the response of the other class (if you need it)
  • Mock the receiver object to check that the call is made

Sometimes, the call you make on other class has side-effects, especially intended side-effects, like persistance, notifications, money-transfers, asset creation...

If this a unit test, how the other class manages it's state is none of your business, what you have to test is that the call is made with the right arguments.

It is the job of the other class tests to check your call as an "incoming command"

The toll Minitest provides us with for this predicament is the Minitest::Mock class. I has two methods of interest:

.expect(...): what method and arguments must we verify (called before the exercise of the method under test)

.verify: Did the expected calls happened as specified? (called after you call your object's method, to see that it triggered the expected external call)

Let's see an example:

# setup
@mock = Minitest::Mock.new
@mock.expect(:outgoing_command, "return_value", [String])

# exercise within your class
@mock.outgoing_command("foo") # => true

# verify at the end
@mock.verify  # => true
Enter fullscreen mode Exit fullscreen mode

There are more elaborate details about how to use .expect on the documentation

The complex thing is usually to get your class to use the mock, which can be done via a previous stub, or dependency injection:

With a previous stub

This is quite common, and not bad if you only have one external dependency with a very stable interface, but if you see yourself having several of these per test, try to decouple your classes and inject your dependencies like the example after this one.

Example taken from Mocking in Ruby with Minitest

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
Enter fullscreen mode Exit fullscreen mode

With Dependency injection

This pattern is not only easier to test, it is also easier to refactor when needing to change collaborator objects, maybe not at the same time for all usages, think Adapter Pattern or feature flag or maybe A/B-test selected strategies.

class UserGreeter
  def initialize(name:, notifier: Slack)
    @name = name
    @notifier = notifier # store replaceable collaborator object
  end

  def run
    msg = whatever(@name)
    @notifier.notify(msg) # use injected dependency by internal name
  end
end

# on the test
require 'test_helper'

class UserGreeterTest < ActiveSupport::TestCase
  test '#run' do
    mock = Minitest::Mock.new
    mock.expect :notify, true, [String]

    user = User.new(
      user_attrs.merge(subscription_service: mock) 
    )
    assert user.run

    assert_mock mock
  end
end
Enter fullscreen mode Exit fullscreen mode

TL;DR

And here as promised I leave you tha handy mnemonic table:

Query Command
incoming result state (+result)
outgoing stub mock

Latest comments (1)

Collapse
 
oinak profile image
Oinak

If you want a js take on this, or just a different point of view to help you understand it better visit: the difference between mocks and stubs explained with js