Technical writings of Shkrt
RSpec’s built-in ‘rspec-mocks’ library contains various abstractions to make available the use of test doubles in our
test suites. These abstractions are known as doubles
, spies
and stubs
. They are extensively used to make test suites
and examples isolated, to decouple tested classes from their dependencies. During isolated test examples test doubles
are staying for collaborator classes, make collaborator classes not involved at all.
The double is an “empty” object that (theoretically) can stand for any other object.
this object is not in any way connected to the real User class and knows nothing about it. The name is just a name.
Basically, this object cannot do anything really useful, except that it will raise an exception if it received the
unexpected message. In this particular case, any message passed to user
would be unexpected.
To make this object actually receive some method calls without an exception, we can initialize the double with predefined values:
Now the test double can respond to load
message and ignores any arguments, passed along with the message.
Another way to deal with messages is to declare a stub
on a test double.
By declaring a stub, we define, which methods can the double respond to, alongside with their respective arguments and return values:
Another and more useful side of stubs is that they can be declared over the existing, “real” class instances and classes:
One of the specifics of the stubs is the working of the expect
clause. To make the test pass, you should first set
the expectation for the message, and then perform the actions that would invoke the message:
If we had put the expectation clause after the invocation of the setup
method, the test would fail for another reason,
because the message would not be received at all. This order of things can seem unnatural at first, because we used to set
the expectations at the end of the test example. To do so, we can use the spies.
The test double has another form of initialization, that makes the test double completely silent regarding the error messages.
There is a shorthand for .as_null_object
:
The spy simply swallows all the messages passed to it, without raising an exception and also without any message at all. But instead, it ‘remembers’ all the messages passed to it during the test example, and therefore allows checking if the messages had been actually received at the end of the example.
All of this mocking patterns and techniques would seem worthless if they cannot provide a possibility to stay in touch
with actually tested interfaces. For example, we can mock the User
class, by creating a double and allowing it to receive
load
method, and then in one of the days, we simply change the name of the method. The test example would remain fully
working, because it works with double, and does not care about changes in the actual class.
To prevent these kinds of failing scenarios, there are verifying doubles concept. The verifying double will check if the mocked class actually can respond to given messages.
The same way we can define verifying double on a class-level messages:
There is also instance_spy
shortcut for instance_double(Klass).as_null_object
,
In this writing, I tried to make a brief overview of the test double concepts available in the RSpec testing framework. All the listed terminology - spy, double, mock is understood differently by different people and it seems that there is no single, unified point of view regarding the naming and usage of different patterns of testing using test doubles. As you may have seen from these short examples, the test doubles are extremely useful to do pure unit-testing, when we have to completely isolate a class from all of its collaborators, yet still having access to their public interface.
Suggested reading:
Effectively Testing with RSpec 3
[ruby
rspec
]