How to Improve Ruby Tests Using RSpec Mocks

Konrad Adamus

Nov 5, 2021 • 28 min read
ruby_tests_rspec_mocks

This article describes the application of mocks in RSpec environment, one of the leading Ruby on Rails test frameworks.

Mocking techniques facilitate the process of writing unit tests and can be of interest to people who want to integrate Test Driven Development into their software development process. The goal of this work is to provide a succinct summary of mocking techniques in RSpec tests along with code examples presenting typical use cases

What is mocking?

Mocking is the practice of using fake objects to mimic the behaviour of real application components. Since mock objects simulate the behaviour of the original components, they can be used during testing of an isolated application code to handle its interaction with other parts of the application.

The benefits of mocking

  • The simplification of the test environment building process. Building objects that return specific values is often easier than initializing and configuring application objects so that they respond in a specific way to messages from the tested code.
  • Minimizing time and resource footprint. The tested code may trigger time and resource consuming operations such as accessing a database. Mock objects can detect a call to a target component and respond in a specified way without actually performing any heavy-duty work.

The main drivers for application of mocks

The top drivers for applications of mocks are test driven development and testing interface between the application and its external components. See the descriptions below:

Test driven development

TDD is a continuous cycle of building tests for a new yet to be implemented feature, implementing the feature, followed optionally by refactoring of both the tests and the feature.

TDD favours building small unit tests that target newly implemented functionalities. An extensive collection of unit tests allows to quickly identify the source of bugs and greatly enhances application refactoring.

The additional benefit of TDD is that creating tests beforehand helps to think of the new functionality in terms of its public interface and facilitates building loosely coupled code. Mocks allow to reach the objective of building an environment for the tests that target exclusively the new functionality.

Testing interface between the application and its external components

Often the application depends on the code outside its repository, for example, the external services providing functionality for sending emails, tracking website traffic.

Performing system tests encompassing both the application and the external services may be impractical due to factors such as significant time delays, high costs of accessing the service. In those cases mocks simulate the working of the external components.

Mocking object types

Various names are used for objects acting as mocks which may lead to confusion. Gerard Meszaros suggested a classification of mocking objects based on the way they act. He uses the term double as the generic term encompassing all variants of mocking objects. The specific double variants were summarized by Martin Fowler, Myron Marston and Ian Dees:

  • Stub – returns pre-programmed results in response to specific messages.
  • Mock – has built-in expectations of messages that it will receive and it will fail if those expectations are not fulfilled.
  • Null object – returns self in response to any message.
  • Spy – registers all messages
  • Fake – a simplified version of an object that operates correctly in development but is unsuitable for production, e.g. an SQLite database.
  • Dummy – an object that is passed around but is never used.

What are RSpec doubles?

RSpec continues the naming convention suggested by Meszaros by using the concept of a double to denote a generic object representing any type of mocking object.

Additionally, RSpec introduces its own division of double types dictating which functionality can be tested. This division is independent from the division into roles, e.g. each of RSpec double types can fulfill the role of a mock or a stub.

Three types of RSpec double

Here are three types of RSpec double: pure double, verifying double, partial double.

1. Pure double (also known as double or normal double)

A pure double can only receive messages that were either allowed or expected, and will cause errors if it receives other messages. It is in no way constrained by the actual object it aims to simulate.

2. Verifying double


It is a variant of a pure double with an additional constraint that its messages are verified against the actual implementation of a specific object.

In other words, a verifying double can be allowed or expected to receive only those messages which are actually implemented in the actual object.

The benefit is that test cases based on verifying doubles won’t suffer from false positives caused by testing of nonexistent methods.

3. Partial double


It is a hybrid of an actual object and test object. The actual object is extended with capabilities to allow and/or expect messages.

The advantage of a partial double is that, unlike pure double or verifying double, it has access to the actual implementation and it can return the original value or modify the original value (see methods and_call_original, and_wrap_original for more details).

Additionally, partial doubles, unlike pure and verifying doubles, are not strict, i.e. you can call methods that were neither allowed nor expected, as long as they are implemented.

Working with pure doubles

Stubbing can be achieved by allowing a pure double to receive a message and return a specific value in response. In the example below, a pure double called random is created. Its goal is to mimic the behaviour of a Random instance. Whenever rand message is received it will return the value of 7.

random_double = double('random')
allow(random_double).to receive(:rand).and_return(7)
expect((1..6).map{ random_double.rand(1..10) }).to eq([7,7,7,7,7,7])

Stubbing can be extended to return a specific sequence of values. Continuing the previous example, the random double was instructed to return 6 numbers from the Fibonacci sequence.

random_double = double('random')
allow(random_double).to receive(:rand).and_return(1,1,2,3,5,8)
expect((1..6).map{ random_double.rand(1..10) }).to eq([1,1,2,3,5,8])

The return value sequence can be defined by an arbitrarily complex algorithm using a block. In the code below, rand message will return the minimum value specified by the range argument. If no range argument is passed, it will return the number 7.

random_double = double('random')
allow(random_double).to receive(:rand) do |arg|
  (arg && (arg.is_a? Range)) ? arg.min : 7
end
expect(random_double.rand).to eq(7)
expect(random_double.rand(1..10)).to eq(1)
expect(random_double.rand(11..20)).to eq(11)

Mocking can be achieved by expecting a pure double to receive a message. This expectation can be extended with respect to message arguments and the number of times a message is sent. In the example below, the random double is expected to receive a rand message 6 times with an argument that is an instance of Range.

random_double = double('random')
expect(random_double).to receive(:rand).with(instance_of(Range))
                                       .exactly(6).times
6.times { random_double.rand(1..10) }

Working with verifying doubles

Verifying doubles differ from pure doubles in that they are defined using either instance_double, class_double or object_double instead of double.

  • Method instance_double receives as an argument a class and it returns a verifying double that can receive only those messages that are implemented in the class as instance methods.
  • Method class_double receives as an argument a class and it returns a verifying double that can receive only those messages that are implemented in the class as class methods.
  • Method object_double receives as argument an object and it returns a verifying double that can receive only those messages that are implemented in the object.

Stubbing an instance method using verifying doubles is presented below. The double stubs rand instance method of Random class.

random_double = instance_double(Random)
allow(random_double).to receive(:rand).and_return(7)
expect((1..6).map{ random_double.rand(1..10) }).to eq([7,7,7,7,7,7])

Mocking an instance method using a verifying double is presented below. The double mocks rand instance method of Random class.

random_double = instance_double(Random)
expect(random_double).to receive(:rand).with(instance_of(Range))
                                       .exactly(6).times
6.times { random_double.rand(1..10) }

The benefit of using verifying doubles is that an attempt to send a message not implemented in the verifying class will cause errors. In the example below, sending random message to the verifying double will produce an error with the following message: “the Random class does not implement the instance method: random”. If double was used instead, the test would have passed.

# This will cause error!
# random_double = instance_double(Random)
# allow(random_double).to receive(:random).and_return(7)
# random_double.random(1..10)

Stubbing a class method using a verifying doubles is presented below. The double stubs rand class method of Random class.

random_double = class_double(Random)
allow(random_double).to receive(:rand).and_return(1,1,2,3,5,8)
expect((1..6).map{ random_double.rand(1..10) }).to eq([1,1,2,3,5,8])

If the verifying double attempts to stub a nonexistent class method, the test will fail. Continuing the previous example, an attempt to stub random class method will produce the following error message: "the Random class does not implement the class method: random".

# This will cause error!
# random_double = class_double(Random)
# allow(random_double).to receive(:random).and_return(1,1,2,3,5,8)
# expect((1..6).map{ random_double.random(1..10) }).to eq([1,1,2,3,5,8])

Working with partial doubles

In the case of partial doubles, unlike in the case of pure and verifying doubles, no reference to the resultant double is returned. Instead, the behaviour of the underlying class is modified to include stubbing and mocking functionality.

Stubbing of class instances can be achieved using allow_any_instance_of method taking as an argument the specific class. In the example below, all instances of Rand class will return the number 7 in response to rand messages.

allow_any_instance_of(Random).to receive(:rand).and_return(7)
expect((1..6).map{ Random.new.rand(1..10) }).to eq([7,7,7,7,7,7])

An arbitrarily complex stubbing algorithm can be described by passing a block as it was the case for pure and verifying doubles. More interestingly, partial doubles, unlike pure and verifying doubles, allow also to base that algorithm on the original implementation using and_wrap_original method.

In the example below, if the instance method rand receives a Range as argument, it will return the original value, otherwise it will return the number 7.

allow_any_instance_of(Random).to receive(:rand).and_wrap_original do |rand_method, arg|
  (arg && (arg.is_a? Range)) ? rand_method.call(arg) : 7
end
expect(Random.new.rand).to eq(7)
expect(Random.new.rand(1..10)).to be_between(1, 10)
expect(Random.new.rand(11..20)).to be_between(11, 20)

Mocking of a single class instance using partial double can be achieved using expect_any_instance_of method. This approach allows for specifying the expected message, arguments and call count. In the example below, an instance of Random is expected to receive rand message with an argument of class Range 6 times.

expect_any_instance_of(Random).to receive(:rand).with(instance_of(Range))
                                                .exactly(6).times
random = Random.new
6.times { random.rand(1..10) }

Mocking of multiple class instances using `expect_any_instance_of` will produce error message similar to “ The message 'rand' was received by #<Random:1500 > but has already been received by #<Random:0x00007f94f01c8ec8>“. In order to mock multiple instances, a combination of a partial double at class level and a verifying double at instance level can be used as described in the chapter regarding use cases.

# This will NOT work!
# expect_any_instance_of(Random).to receive(:rand).with(instance_of(Range))
#                                                .exactly(6).times
# 6.times { Random.new.rand(1..10) }

It is worth noting that stubbing or mocking a method of a class instance using a partial double will not call the original code. The other methods, which were not mocked, will call the original code. See the example below.

expect_any_instance_of(Array).to receive(:append).with(instance_of(Integer)).once
arr = []
arr.append(1)
arr << 2
expect(arr).to eq [2]

In order for the original code of the stubbed or mocked method to be called use and_call_original.

expect_any_instance_of(Array).to receive(:append).with(instance_of(Integer))
                                                 .once
                                                 .and_call_original
arr = []
arr.append(1)
arr << 2
expect(arr).to eq [1, 2]

Stubbing a class is presented in the example below.

allow(Random).to receive(:rand).and_return(7)
expect(Random.rand).to eq(7)

Mocking a class is presented in the example below.

expect(Random).to receive(:rand).exactly(6).times
6.times { Random.rand }

Matching arguments

Mocks can be constrained to match specific arguments using with method. This method accepts a list of arguments which should match the list of arguments received in a message by a stubbed or mocked object.

expect_any_instance_of(Array).to receive(:append).with(1, 2, 'c', true)
Array.new.append(1, 2, 'c', true)

It is also possible to use RSpec matchers that will allow different argument variants depending on the specific matcher.

expect_any_instance_of(Array).to receive(:append).with(
                                                        instance_of(Integer), 
                                                        kind_of(Numeric), 
                                                        /c+/, 
                                                        boolean
                                                      ).twice.and_call_original
Array.new.append(1, 2, 'c', true)
         .append(2, 3.0, 'cc', false)

In the example above 4 different matchers were applied:

  • instance_of - will match any instance of a specified class, e.g. instance_of(Integer) will match any integer and will not match any float
  • kind_of - will match any instance who has in its ancestor list a specified class or module, e.g. kind_of(Numeric) will match any number extending Numeric including integers and floats
  • Regexp instance - will match any string that matches the specified regular expression
  • boolean - will match any boolean value i.e. true or false

Matcher corresponding to any argument can also be used. The example below will match any arguments as long as there are 4 of them.

expect_any_instance_of(Array).to receive(:append).with(
                                                        anything, 
                                                        anything,
                                                        anything, 
                                                        anything
                                                      ).twice.and_call_original
Array.new.append(1, 2, 'c', true)
          .append(2, 3.0, 'cc', false)

Use any_args to match an arbitrarily long (or empty) list of any arguments. The example below will match any arguments as long the last one is true.

expect_any_instance_of(Array).to receive(:append).with(any_args, true)
                                                 .thrice.and_call_original
Array.new.append(1, 2, 'c', true)
          .append(1, 2, 3, {}, [], true)
          .append(true)

Matching hashes can be performed using hash_including matcher. The example below will match any hash that includes keys :nationality, :addresss, :name.

expect_any_instance_of(Array).to receive(:append)
                                  .with(hash_including(
                                                        :nationality, 
                                                        :address, 
                                                        :name
                                                      ))
[].append({ name: 'Alice', address: {}, nationality: 'American', 
            occupation: 'engineer' })

Keys as well as key-value pairs can be expected . Both variants can be mixed as long the key-value pairs are at the end of the argument list of hash_including as presented in the example below.

expect_any_instance_of(Array).to receive(:append)
                                    .with(
                                      hash_including(
                                        :nationality, 
                                        :address, 
                                        name: 'Alice'
                                      ))
[].append({ name: 'Alice', address: { city: 'Miami', street: 'NW 12th Ave' }, 
            nationality: 'American' })

Nesting of hash_including matchers is allowed. See the example below.

expect_any_instance_of(Array).to receive(:append)
                                    .with(
                                      hash_including(
                                        :nationality,
                                        address: hash_including(:city, :street),
                                        name: 'Alice'
                                      ))
[].append({ name: 'Alice', address: { city: 'Miami', street: 'NW 12th Ave' }, 
            nationality: 'American' })

In order to match arrays array_including matcher can be applied, in a fashion similar to hash_including.

In most cases different matchers can be mixed, e.g. hash_including can use instance_of as a matcher for values of some of its elements. The overview of RSpec matchers can be found here.

Looking further

The presented examples are foundational building blocks that can be, with some constraints, mixed to produce final test implementation. For example:

  • Stubbing functionality (what should be returned) can be mixed with mocking functionality (what are the expectations regarding messages).

expect_any_instance_of(Random).to receive(:rand)
                                    .with(instance_of(Range))
                                    .exactly(6).times
                                    .and_return(1,1,2,3,5,8)
  • Partial doubles can be mixed with verifying doubles.

random_double = instance_double(Random)
allow(random_double).to receive(:rand).and_return(1,1,2,3,5,8)
allow(Random).to receive(:new).and_return(random_double)

The more complex use cases will be presented in the next section.

Use cases: a simple polling application

The use cases presented in this section implement tests of a simple polling application.

Application outline

The application is built purely for educational purposes and because of its simplification has no real uses. The implementation details can be found in the appendix.

The application comprises 3 classes:

  • Feedback represents a form allowing a user to evaluate a subject using like and dislike actions. A feedback can be nudged using nudge! method. The nudging is a process in which poll creators try to influence a user's evaluation e.g. by modifying a question.
  • Participant represents a user taking part in a poll. The user evaluates a given subject using a feedback form.
  • Poll represents a polling process. It takes as input names of participants and subjects to be evaluated. Based on these it creates participants and feedbacks. It can run a poll by asking each participant to evaluate each of the subjects using feedback forms. The poll nudges one of the feedbacks with the aim of skewing the evaluation results. Once the poll is finished, it produces the poll outcome in the form of subject ranking sorted according to the number of likes.

The test code has no access to the internals of the polling process, but it can use RSpec doubles to get insight as to how the polling process proceeds.

Test setup

By default tests use the following initialization code.

RSpec.describe Poll do
  let(:names) { %w(alice adam peter kate) }
  let(:subjects) { %w(math physics history biology) }
  subject { described_class.new(names: names, subjects: subjects) }

An RSpec subject contains the reference to an instance of Poll that has received the list of 4 participant names and the list of 4 subjects to be evaluated. Here the RSpec subject belongs to the domain of RSpec testing environment and it should not be confused with subjects which belong to the domain of polling application.

RSpec subject denotes the subject of tests and will be used by individual tests, while subjects will be passed to feedbacks which in turn will be evaluated by participants in the polling application.

When necessary input names and subjects can be modified by overriding let definitions in individual tests, for example:

context 'when 2 participant and 4 subjects' do
  let(:names) { %w(alice adam) }

  it 'instantiates 2 participants' do
    # TEST IMPLEMENTATION
  end
end

Scenario: test a class has been instantiated

Suppose we need to test that participants are instantiated with proper arguments upon poll instantiation.

Variant 1. Instances don’t receive messages

We can use partial double - expect(Participant). The with method validates that correct arguments have been passed. Each set of arguments should be sent once. From the perspective of the test code, the order of object instantiation doesn’t matter.

context 'when 2 participant and 4 subjects' do
  let(:names) { %w(alice adam) }

  it 'instantiates 2 participants' do
    expect(Participant).to receive(:new).with(hash_including(name: 'adam')).once
    expect(Participant).to receive(:new).with(hash_including(name: 'alice')).once
    subject
  end
end

Variant 2. Instances receive messages

In the previous variant sending messages to newly instantiated Participant objects would cause errors, since calling a new method on partial double returns nil. We can fix this by applying and_call_original which will cause partial double to return the same value as if the method was called on the original class, i.e. the valid Participant object.

context 'when 2 participant and 4 subjects' do
  let(:names) { %w(alice adam) }

  it 'instantiates 2 participants' do
    expect(Participant).to receive(:new).with(hash_including(name: 'adam'))
                                        .and_call_original.once
    expect(Participant).to receive(:new).with(hash_including(name: 'alice'))
                                        .and_call_original.once
    subject.run
  end
end

Scenario: test instances of a class

Suppose we need to test that an instance of Participant receives requests for evaluation.

Variant 1. One instance

In the case that messages will be sent to only one instance of Participat, we can use expect_any_instance_of partial double. Similarly to previous examples with is used for validating method arguments, exactly(n).times is used for specifying the number of evaluate messages received.

context 'when 1 participant and 4 subjects' do
  let(:names) { %w(alice) }

  it 'should ask participant for evaluation 4 times' do
    expect_any_instance_of(Participant).to receive(:evaluate)
                                  .with(instance_of(Feedback)).exactly(4).times
    subject.run
  end
end

Variant 2. Multiple instances

In the case that messages will be sent to multiple instances of Participant, expect_any_instance_of partial double will fail with message similar to the following: The message 'evaluate' was received by #<Participant:1720 @name=adam> but has already been received by #<Participant:0x00007fea7f091368>

To solve this we can use a verifying double instance_double(Participant) and allow it to receive evaluate messages. Additionally we should stub Participant class to return the verifying double in response to new message. Each newly instantiated Participant will actually be one and the same object i.e. the verifying double. We can then register calls to any number of Participant instances.

context 'when 4 participants and 4 subjects' do
  it 'should ask participants for evaluation 16 times' do
    fake_participant = instance_double(Participant)
    allow(fake_participant).to receive(:evaluate)
    allow(Participant).to receive(:new).and_return(fake_participant)

    expect(fake_participant).to receive(:evaluate)
                                  .with(instance_of(Feedback)).exactly(16).times
    subject.run
  end
end

Scenario: induce error in one of the instances

Suppose that we want to test the behaviour of Poll when one of many Participant instances fails to evaluate. We can use partial double allow_any_instance_of combined with and_wrap_original method as presented below.

The block passed to and_wrap_original receives Method object, representing the evaluate method, as the first argument. The remaining arguments correspond to arguments originally passed to the evaluate method. Inside the block, we can identify a specific Participant object, on which evaluate was called, by examining the evaluate method's receiver e.g. examining the participant’s name.

In the snippet below we send an error (false) when a participant named Adam receives math for evaluation. Otherwise, the original evaluate method receiving the original arguments is called.

context 'when 1 participant fails to evaluate 1 subject' do
  context 'when 4 participants and 4 subjects' do
    it 'produces 1 error' do
      allow_any_instance_of(Participant).to receive(:evaluate)
                                              .and_wrap_original do |evaluate_method, feedback|
        is_adam = evaluate_method.receiver.name == 'adam'
        is_math = feedback.subject == 'math'
        (is_adam && is_math) ? false : evaluate_method.call(feedback)
      end
      expect { subject.run }.to change { subject.error_count }.from(0).to(1)
                            .and change{ subject.vote_count }.from(0).to(15)
    end
  end
end

Scenario: test instances with expected arguments not known in advance

Suppose we need to test that an instance of Feedback receives a nudge! message with a specific argument once, but we have no way of knowing what that argument is until the poll ends. We can’t expect what the argument will be before the poll is run, as we did in the previous examples.

Variant 1. Using spy

To achieve this goal we can use a verifying spy instance_spy(Feedback) which will track all messages sent. Additionally we will use partial double Feedback and stub it to return the verifying spy in response to a new message. Each newly instantiated Feedback will actually be one and the same object i.e. the verifying spy. Since the spy registers received messages and the corresponding arguments, we will be able to examine it after the poll has ended.

context 'when 4 participants and 4 subjects' do
  it 'nudges one feedback' do
    fake_feedback = instance_spy(Feedback)
    allow(Feedback).to receive(:new).and_return(fake_feedback)
    nudge_template = subject.run
    expect(fake_feedback).to have_received(:nudge!).with(nudge_template).once
  end
end

Variant 2. Using double

The same objective can be achieved using a verifying double instance_double(Feedback) with one exception. We will have to explicitly define which messages the double is allowed to receive. In terms of results both variants are equivalent, however verifying double requires more work in terms of test setup.

context 'when 4 participants and 4 subjects' do
  it 'nudges one feedback' do
    fake_feedback = instance_double(Feedback)
    allow(fake_feedback).to receive(:nudge!)
    allow(fake_feedback).to receive(:nudged?)
    allow(fake_feedback).to receive(:like)
    allow(fake_feedback).to receive(:dislike)
    allow(Feedback).to receive(:new).and_return(fake_feedback)
    nudge_template = subject.run
    expect(fake_feedback).to have_received(:nudge!).with(nudge_template).once
  end
end

Summary

The following aspects of RSpec mocking were presented:

  • The advantages of mocking techniques, their origin and types.
  • The application of mocking in RSpec and their division into pure, verifying and partial doubles, as well as matching arguments of doubles.
  • The summary of how to build tests using RSpec doubles for a selection of typical use cases.

Together the discussed facets of RSpec mocking will help you to enhance your unit testing skills and strengthen your test driven development of Ruby code.

Appendix

The application comprises 4 files. It requires that RSpec 3 is installed. The tests can be run using:rspec poll_spec.rb

feedback.rb

class Feedback
  attr_reader :subject, :likes, :dislikes

  def initialize(**args)
    @subject = args[:subject] || 'default'
    @likes, @dislikes = 0, 0
    @nudge = nil
  end

  def like
    @likes += 1
  end

  def dislike
    @dislikes += 1
  end

  def nudge!(data)
    @nudge = data
  end

  def nudged?
    @nudge
  end
end

participant.rb

class Participant
  attr_reader :name

  def initialize(**args)
    @name = args[:name] || 'anonymous'
  end

  def evaluate(feedback)
    like_factor = feedback.nudged? ? 10 : 1
    ((rand 0..like_factor) > 0) ? feedback.like : feedback.dislike
  end
end

poll.rb

require './feedback.rb'
require './participant.rb'

class Poll
  attr_reader :vote_count, :error_count

  def initialize(**args)
    @feedbacks = args[:subjects].map{|subject| Feedback.new(subject: subject) }
    @participants = args[:names].map{|name| Participant.new(name: name)}
    @error_count = 0
  end

  def run
    @feedbacks.sample.nudge!(nudge_template)
    @feedbacks.each do |feedback|
      @participants.each do |participant|
        @error_count += 1 unless participant.evaluate(feedback)
      end
    end
    nudge_template
  end

  def vote_count
    @feedbacks.inject(0) do |memo, feedback|
      memo += (feedback.likes + feedback.dislikes)
    end
  end

  def ranking
    @feedbacks.sort{|a,b| b.likes <=> a.likes }
  end

  private

  def nudge_template
    @nudge_template ||= ('nudge template ' << rand(1..10).to_s)
  end
end

poll_spec.rb

require './poll.rb'

RSpec.describe Poll do
  let(:names) { %w(alice adam peter kate) }
  let(:subjects) { %w(math physics history biology) }
  subject { described_class.new(names: names, subjects: subjects) }

  describe 'test a class has been instantiated' do
    context 'instances do NOT receive messages' do
      context 'when 2 participant and 4 subjects' do
        let(:names) { %w(alice adam) }

        it 'instantiates 2 participants' do
          expect(Participant).to receive(:new).with(hash_including(name: 'adam')).once
          expect(Participant).to receive(:new).with(hash_including(name: 'alice')).once
          subject
        end
      end
    end

    context 'instances receive messages' do
      context 'when 2 participant and 4 subjects' do
        let(:names) { %w(alice adam) }

        it 'instantiates 2 participants' do
          expect(Participant).to receive(:new).with(hash_including(name: 'adam'))
                                              .and_call_original.once
          expect(Participant).to receive(:new).with(hash_including(name: 'alice'))
                                              .and_call_original.once
          subject.run
        end
      end
    end
  end

  describe 'test instances of a class' do
    context 'single instances' do
      context 'when 1 participant and 4 subjects' do
        let(:names) { %w(alice) }

        it 'asks participant for evaluation 4 times' do
          expect_any_instance_of(Participant).to receive(:evaluate)
                                                   .with(instance_of(Feedback)).exactly(4).times
          subject.run
        end
      end
    end

    context 'multiple instances' do
      context 'when 4 participants and 4 subjects' do
        it 'asks participants for evaluation 16 times' do
          fake_participant = instance_double(Participant)
          allow(fake_participant).to receive(:evaluate)
          allow(Participant).to receive(:new).and_return(fake_participant)

          expect(fake_participant).to receive(:evaluate)
            .with(instance_of(Feedback)).exactly(16).times
          subject.run
        end
      end
    end
  end

  describe 'induce error in one of the instances' do
    context 'when 1 participant fails to evaluate 1 subject' do
      context 'when 4 participants and 4 subjects' do
        it 'produces 1 error' do
          allow_any_instance_of(Participant).to receive(:evaluate)
                                                  .and_wrap_original do |evaluate_method, feedback|
            is_adam = evaluate_method.receiver.name == 'adam'
            is_math = feedback.subject == 'math'
            (is_adam && is_math) ? false : evaluate_method.call(feedback)
          end
          expect { subject.run }.to change { subject.error_count }.from(0).to(1)
                                .and change{ subject.vote_count }.from(0).to(15)
        end
      end
    end
  end

  describe 'test instances with expected arguments not known in advance' do
    context 'using spy' do
      context 'when 4 participants and 4 subjects' do
        it 'nudges one feedback' do
          fake_feedback = instance_spy(Feedback)
          allow(Feedback).to receive(:new).and_return(fake_feedback)
          nudge_template = subject.run
          expect(fake_feedback).to have_received(:nudge!).with(nudge_template).once
        end
      end
    end

    context 'using double' do
      context 'when 4 participants and 4 subjects' do
        it 'nudges one feedback' do
          fake_feedback = instance_double(Feedback)
          allow(fake_feedback).to receive(:nudge!)
          allow(fake_feedback).to receive(:nudged?)
          allow(fake_feedback).to receive(:like)
          allow(fake_feedback).to receive(:dislike)
          allow(Feedback).to receive(:new).and_return(fake_feedback)
          nudge_template = subject.run
          expect(fake_feedback).to have_received(:nudge!).with(nudge_template).once
        end
      end
    end
  end
end

Related topics

More posts by this author

Konrad Adamus

Konrad started his engineering career as a Software Developer building digital television systems...
New call-to-action