Intro to RSpec Mocks
Tags: ruby, rspec, testing
Published: 2022-09-22 22:07:24

*This article was originally written by [Abiodun Olowode](https://www.honeybadger.io/blog/authors/abiodunadukeajibade/) on the [Honeybadger Developer Blog](https://www.honeybadger.io/blog/rspec-mocks/).*

Generally speaking, a mock is a replica or imitation of something. RSpec mocks, in the same sense, are an imitation of return values or method implementations. The ability to carry out this kind of imitation makes it possible to set expectations that specific messages are received by an object. Amazing, right? 😃
RSpec mocks create test doubles with which we can implement our imitations, although these imitations can also be carried out on objects that are part of one's system.

## What are Test Doubles?

According to the book **xUnit Patterns**, which I believe offers a perfectly good explanation, test doubles can be likened to a stunt double.

> When the movie industry wants to film something that is potentially risky or dangerous for the leading actor to carry out, they hire a "stunt double" to take the place of the actor in the scene. The stunt double is a highly trained individual who is capable of meeting the specific requirements of the scene. They may not be able to act, but they know how to fall from great heights, crash a car, or whatever the scene calls for. How closely the stunt double needs to resemble the actor depends on the nature of the scene. Usually, things can be arranged such that someone who vaguely resembles the actor in stature can take their place.

For example, if one needs to make a call to an external API in a certain class, it's very likely that during testing, one will want to avoid this API call. In this case, a "stunt double" can be created to simulate the API response.

## Creating my First Test Double

Let's start by creating two new files named `crypto.rb` and `trader_spec.rb` within a folder named `rspec_mocks`. Within this folder, we install the needed dependencies.

```ruby
gem install rspec       # for rspec-core, rspec-expectations, rspec-mocks
gem install rspec-mocks # for rspec-mocks only
```

In our `trader_spec.rb` file, let's create tests that determine whether a certain crypto trader with a current coin worth is able to buy a new coin and double that coin's value by partaking in a promo. Of course, this would, in turn, lead to an increase in his total coin worth.

```ruby
# trader_spec.rb

require_relative 'crypto'

describe Trader do

  before(:each) do
    @trader = Trader.new(50)
  end

  describe '#partake in promo' do
    it 'should increase his coin worth' do
      new_coin = Coin.new(10)
      @trader.partake_in_promo(new_coin)

      expect(@trader.coin_worth).to eq(70)
    end
  end
end
```

In the above test, we are testing that a trader is created with an initial coin worth `50`; he purchases a coin of value `10` and partakes in a promo that doubles the coin value to `20` which, in turn, leads to a total coin worth of `70` (50 + 20).

Within the rspec_mocks folder, let's run our tests using the command `rspec trader_spec.rb`. Without a trader class and any methods, our test returns an error, of course. Now, let's proceed to write the code that makes this test pass.
In our `crypto.rb` file, let's create the trader class as follows:

```ruby
class Trader
  attr_accessor :coin_worth

  def initialize(coin_worth)
    @coin_worth = coin_worth
  end

  def partake_in_promo(coin)
    coin_new_value = coin.win_promo
    @coin_worth += coin_new_value
  end
end
```

We also have to create the corresponding Coin class:

```ruby
class Coin
  attr_accessor :value
  def initialize(value)
    @value = value
  end

  def win_promo
    @value = @value * 2
  end
end
```

Running our test at this point gives an all green.

However, when testing, we do not want our tests to fail or pass based on the behavior of an object outside the scope of the class being tested. For example, if the `win_promo` method changes within the `Coin` class, and `value * 2` becomes `value + 2`, our tests fail. This is discouraging because the failure is only due to an implementation change within a dependency rather than any inconsistencies on the part of the Trader class. The test for the coin value doubling should be left for a Coin spec and not done within the Trader spec so that we're able to keep our tests within scope.

Let's go further and create a double for our coin by replacing `new_coin = Coin.new(10)` with `new_coin = double('Coin')`. Running our test again gives the following error:

![Coin double error](https://www.honeybadger.io/images/blog/posts/rspec-mocks/coin-double-error.png)

This error tells us that our double has no access to a certain `win_promo` method; this is why it is called an unexpected message. As a result, we need to pass this message to our double and signify the expected response, which is 20 in our case.

```ruby
new_coin = double('Coin', :win_promo => 20)
```

Running our test again gives us a green 💪.

Let's carry out a little experiment. Let's go ahead and change the `win_promo` method name to `win_a_promo`. Surprisingly, our tests still pass, but with a real object, we should get a `no_method` error. This happens because `doubles` are not an exact replica of the object they imitate. However, RSpec provides the `instance_double` method, which verifies whether doubles are an exact representation of the object they are "impersonating".
Using the `instance_double` method, we have the following:

```ruby
new_coin = instance_double('Coin', :win_promo => 20)
```

Running our test generates the following error:

![No instance method](https://www.honeybadger.io/images/blog/posts/rspec-mocks/no-instance-method.png)

Hence, we can test an object in scope without testing the behavior of dependencies and still validate them against real objects such that they are throwing the same errors expected of real objects. Reverting the method name to `win_promo` causes the tests to pass.

## Method Stubs

A method stub is an instruction to an object (real or test double) to return a specific value in response to a message. Let's put this into action by trying to withdraw some coins. In our case, before any withdrawal, we should ensure that a trader is verified.

```ruby
describe "withdraw some coins" do

  it 'should be successful if trader is verified' do
    allow(@trader).to receive(:verified?) { true }
    @trader.withdraw(20)

    expect(@trader.coin_worth).to eq(30)
  end

  it 'should not be successful if trader is unverified' do
    allow(@trader).to receive(:verified?) { false }
    @trader.withdraw(20)

    expect(@trader.coin_worth).to eq(50)
  end
end
```

Above, we have written a test case for a successful withdrawal when a trader is verified, as well as an unsuccessful one, when he is not. The statement `allow(@trader).to receive(:verified?) { false }` translates to allowing the trader object to receive the message `verified?`, which in turn calls a `verified?` method and returns a response of `false`. What this means is that we are dictating the result of a particular method called on an object or a double. Running our tests now would cause a failure because we have not implemented the `withdraw` method for the Trader class.

The ensuing code can be thus:

```ruby
# in crypto.rb, within the trader class

def withdraw(value)
  return unless verified?
  
  @coin_worth -= value
end

```

When we run our tests, they pass, even without an actual `verified?` method in our Trader class. This is because we have specified in our stub, the method name and the result to be returned; hence, RSpec sees no need to call that method within the class to obtain a result.

In real-life cases, though, this method would exist, and maybe within it, there may be other dependencies totally unrelated to the class being tested. Hence, we can say that method stubs are especially useful when we have other dependencies that we do not want tested, such as external APIs. We can skip testing these by dictating a known value as the result of that method call thanks to method stubs!!

<!-- Message Expectations -->
## Message Expectations

The terms message and method are often used synonymously, but they do have a slight difference. In object-oriented programming, objects communicate by sending messages to one another. When an object receives a message, it invokes a method with the same name as that message. An example is shown below:

```ruby
class User
  def save
  end
end
```

When an instance of the class `User` called `user` is created, we can pass a `save` message to it via `user.save`. This in turn calls the `save` method defined within the `User` class.

A message expectation is an expectation that an object should receive a certain message before the example ends. Similar to the `user.save` example, one would be testing that the `user` object receives a `save` message. Let's implement this with our trader class.

Remember our `verified?` method? Let's go ahead and actually implement the verification. Let's assume that during sign up as a trader, a verification center has to be picked, where one would submit all his documents and also carry out some biometric analysis. This external service would be responsible for checking that a trader has been verified (i.e., submitted all documents and done his biometrics).

Our `before_each` method would change to incorporate the new changes:

```ruby
before(:each) do
  @verification_center = VerificationCenter.new
  @trader = Trader.new(50, @verification_center)
end
```

We should send a `verify` message to the verification center with the trader to be verified as an argument. The corresponding test would be as follows:

```ruby
describe 'verification is carried out' do
  it 'should call the verify method to verify a trader' do
  expect(@verification_center).to receive(:verify).with(@trader)

  @trader.verified?

  end
end
```

In the above test, we're saying that when the `verified?` message is sent, we expect that the `verified?` method should send a `verify` message to the verification center with the trader as an argument. If this statement is not quite clear, check [above](#message-expectations) and read about messages and methods.

To make this test pass, we would implement the following changes to our code:

```ruby
attr_accessor :coin_worth, :verification_center

def initialize(coin_worth, verification_center)
  @coin_worth = coin_worth
  @verification_center = verification_center
end

def verified?
  @verification_center.verify(self)
end

```

Let's add the verification center class:

```ruby
class VerificationCenter
  def verify(trader)
  # checks if the trader is verified using an external api
  end
end
```

Now, our tests pass. We have been able to confirm that the verification center received the `verify` message with the trader as an argument. This is useful because we are not concerned with testing the verification center class at this time; we just test that the message was received and the right arguments were sent.

If we want to set a response other than `nil` from the verify message within our test, it can be carried out using `and_return` or passing the response as a block.

```ruby
expect(@verification_center).to receive(:verify).with(@trader).and_return("return value")
expect(@verification_center).to receive(:verify).with(@trader) {"return value"}
```

When could setting responses be needed? If we wanted to test that we do not try to verify a trader more than once during a particular session, we can check that the `verify` method is called only once.

```ruby
describe 'verification is carried out' do
  it 'should call the verify method to verify a trader' do
    expect(@verification_center).to receive(:verify).with(@trader).and_return(true).once

    @trader.verified?

    @trader.verified?
  end
end
```

Running the test now yields the following:

![Called twice error](https://www.honeybadger.io/images/blog/posts/rspec-mocks/called-twice-error.png)

 To make this test pass, it is imperative that we update our `verified?` method to use an instance variable if it has been previously called.

```ruby
def verified?
  @verified ||= @verification_center.verify(self)
end
```

Our tests pass now because by returning `true`, we assign a value to our instance variable `@verified`. We make just one trip to the verification center, and we can test that this happens using message expectations.

## Test Spies

With message expectations, we talk in terms of the future; we already declare our expectation of the message to be received, and then we carry out the actions that would make that possible. This is why we have the `expect` statement before the `trader.verified?` action. What if we wanted to carry out the actions first, and then spy on the object during the process to enable us to check that messages were received with an accurate count?
This is where test spies come in; as opposed to testing that an object will receive a message, we test that an object received a message. To do this (verify that a message was received), the given object must be setup to spy on it.

Implementing the previous test using spies, we would create a spy verification center and specify a return value for the message `verify`.

```ruby
describe 'verification is carried out' do
  it 'should call the verify method to verify a trader' do
    verification_center = spy("Verification center", :verify => true)
    @trader.verification_center = verification_center
    @trader.verified?
    @trader.verified?

    expect(verification_center).to have_received(:verify).once.with(@trader)

  end
end
```

As seen above, test spies determine whether a message was received, while message expectations test whether a message will be received.

## Conclusion

RSpec gives us a wide range of tools that enable us to test the behavior of our objects. Mocks are useful in testing the interaction between objects, verifying what messages are sent or not sent, and ensuring that they communicate as intended. Stubs are particularly useful in imitating a return value of objects when you're more interested in their results rather than their behavior, such as an external API. Spies also test the interaction between objects but carry out checks that events happened rather than testing that they will happen sometime in the future. Being able to identify, at all times, what behaviors are to be tested and their dependencies, as well as whether testing those dependencies are relevant, goes a long way towards helping determine what tool to use each time.