-
Notifications
You must be signed in to change notification settings - Fork 5
Doubles
Doubles are used as stand-in objects. They handle any and all method calls. Doubles are ideal when an object or method needs a "thing" with no type restriction. In other words, doubles can be used for duck-typing.
Note: This documentation describes the syntax introduced in Spectator v0.11
Doubles must be defined outside of a test before they can be instantiated.
This can be done by using the double
keyword.
It takes at least one argument - the name of the double.
This name will be used to reference the double later.
describe "#something" do
# Define a double named `:my_double`.
double :my_double
it "does something" do
# Use double here.
end
end
To make it clearer, def_double
can be used instead of double
to define a double.
def_double :my_double
The name of a double can be a symbol, string, or type name. Keep in mind though, the name has no effect on the functionality. The name is used to reference the double and to identify it in error output. Using a type name for a double will not bind the double to that type in any way. If functionality like this is needed, see if a mock might be better.
After the double's name, default stubs can be provided with key-value pairs (keyword arguments).
double :my_double, answer: 42, foo: "bar"
This is a quick way to define default stubs.
The key is the method name and the value is the return value.
Stubs defined in this way will respond to any call with the same method name.
For instance, calling answer
, answer("test")
and answer(key: "value")
will return 42.
Even passing a block to answer
will return 42 (though the block won't be yielded to).
Stubs defined with key-value pairs (keyword arguments) are generally static.
To define more complex and stricter default stubs, provide a block.
Stubs defined in this way require the method call to match the signature exactly.
If it doesn't an UnexpectedMessage
error may be raised.
The syntax for defining a default stub is to define a method like normal (with def
) and prefix it with stub
.
For example:
stub def some_method(arg)
# ...
end
Here's a sample of some default stubs:
double :my_double do
stub def current_time
Time.utc
end
stub def exact_args(arg1, *, kwarg)
"#{kwarg}: #{arg1}"
end
stub def do_twice
yield
yield
end
end
The key-value pairs (keyword arguments) and block-based stubs can be combined, even with the same method name.
double :my_double, answer: 42 do
stub def answer(arg1, arg2)
arg1 + arg2
end
stub non_answer
0
end
end
In the example above, calling answer(1, 2)
will return 3, but with any other arguments, it will return 42.
Stubs defined in the block take precedence.
Please read the documentation regarding stubs for more information.
Then to use the double in a test, use double
with the name used in the definition.
it "does something" do
dbl = double(:my_double)
end
Like def_double
, new_double
can be used instead.
it "does something" do
dbl = new_double(:my_double)
end
Default stubs can be defined with key-value pairs (keyword arguments) when instantiating a double. This is similar to the default stubs when a double is defined, but with some minor differences:
- The block syntax cannot be used.
- Stubs defined during initialization override any previously defined stubs.
- Memoized values (
let
andsubject
) can be used.
let(answer) { 42 }
double :my_double, answer: 5 do
def answer(arg1, arg2)
arg1 + arg2
end
end
it "does something" do
dbl = double(:my_double, answer: answer)
expect(dbl.answer).to eq(42)
expect(dbl.answer(1, 2)).to eq(3)
end
Doubles can also be instantiated and used within hooks and memoize (e.g. let
) blocks.
In addition to the methods already described, doubles can have stubs defined after they're instantiated.
One way this can be done is with the allow
keyword.
it "does something" do
dbl = double(:my_double)
allow(dbl).to receive(:answer).and_return(42)
expect(dbl.answer).to eq(42)
end
More information can be found in the stub documentation.
A usage of doubles may be to ensure the test system calls some methods on an object. Take for instance this class:
class Emitter
def initialize(@value : Int32)
end
def emit(target)
target.call(@value)
end
end
A double can be used to ensure emit
invokes call
on target
.
Here's the initial code for the spec:
describe Emitter do
subject { Emitter.new(42) }
describe "#emit" do
it "invokes #call on the target" do
# ...
end
end
end
First, define the double.
double :target, call: nil
Then instantiate it.
it "invokes #call on the target" do
target = double(:target)
end
and finally, call emit
and verify that target
received call
.
This is done with the have_received
matcher.
it "invokes #call on the target" do
target = double(:target)
subject.emit(target)
expect(target).to have_received(:call)
end
In the snippet above, it is only being verified that call
was received.
This doesn't specify how many times or the arguments it was called with.
Arguments can be checked by specifying the with
modifier.
it "invokes #call on the target" do
target = double(:target)
subject.emit(target)
expect(target).to have_received(:call).with(42)
end
Please see the have_received
matcher documentation for more information.
There's a short-hand syntax that can merge allow
and expect
together.
For instance, with the following code:
it "does something" do
dbl = double(:my_double)
allow(dbl).to receive(:answer).and_return(42) # Merge this line...
dbl.answer
expect(dbl).to have_received(:answer) # and this line.
end
This can be reduced to:
it "does something" do
dbl = double(:my_double)
expect(dbl).to receive(:answer).and_return(42)
dbl.answer
end
This "expect-receive" syntax will define a stub on the double and expect it to be called eventually.
Sometimes it's necessary to access the class methods of a double. For instance, when testing a method that relies on the class methods.
def call_class_method(klass : T.class) forall T
klass.something
end
An instance of a double cannot be passed to this method.
But a class double can.
To retrieve a class double, use class_double
instead of double
(or new_double
) when instantiating the double.
klass = class_double(:my_double)
class_double
returns the type of the referenced double.
The following is true: class_double(:my_double) == new_double(:my_double).class
Class stubs can have stubs defined on them, just like normal doubles.
However, the class methods must be defined in the definition block before they can be used.
Instead of raising an UnexpectedMessage
error,
a compile-time error will occur when attempting to call a class double method that doesn't exist.
double :my_double do
# Define class methods with `self.` prefix.
stub def self.something
42
end
end
it "does something" do
# Default stubs can be defined with key-value pairs (keyword arguments).
dbl = class_double(:my_double, something: 3)
expect(dbl.something).to eq(3)
# Stubs can be changed with `allow`.
allow(dbl).to receive(:something).and_return(5)
expect(dbl.something).to eq(5)
# Even the expect-receive syntax works.
expect(dbl).to receive(:something).and_return(7)
dbl.something
end
There are some variants of doubles that have different behavior. Anonymous Doubles can be used for quick stand-ins and don't require a full definition. Null Objects returns itself for undefined methods.