How to Improve Ruby Tests Using RSpec Mocks
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 a mock object simulates the behaviour of the original component, it 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 real 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 a built-in message expectation of messaging 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 mocks and stubs.
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 bythe real 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 code 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 floatkind_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 floatsRegexp instance
- will match any string that matches the specified regular expressionboolean
- 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
Enhance your tests with RSpec mocks
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