Skip to content
Mike Miller edited this page Jul 14, 2022 · 7 revisions

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

Defining a Double

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.

Instantiating a Double

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 and subject) 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.

Defining Behavior

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.

Expecting Behavior

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.

Class Doubles

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.

Clone this wiki locally