Skip to content

Mocks and doubles

Mike Miller requested to merge mocks-and-doubles into release/0.9

This is the initial pass at #6 (closed) . It contains the majority of features that RSpec has in regards to mocking.

There are two conceptual types added: mocks and doubles. A mock wraps an existing type, whereas a double is its own type. Both support stubbing methods.

There are various types of doubles. Plain doubles must be defined before they can be used. The syntax is as follows:

double :my_double do
  stub my_method
end

it "returns nil" do
  dbl = double(:my_double)
  allow(dbl).to receive(:my_method)
  expect(dbl.my_method).to be_nil
end

The double is given an identifier, which is :my_double in this case. The same identifier must be used when defining and instantiating a double. Methods on doubles are defined by using stubs. Stubs are very flexible in their syntax. A default implementation can be specified by providing a block.

double :my_double do
  stub one_arg(arg) : Int32
  stub splat(*args) : Int32
  stub add(left, right) { left + right }
end

it "takes an argument" do
  dbl = double(:my_double)
  allow(dbl).to receive(:one_arg).and_return(42)
  expect(dbl.one_arg(0)).to eq(42)
end

it "takes splat arguments" do
  dbl = double(:my_double)
  allow(dbl).to receive(:splat).and_return(42)
  expect(dbl.splat(1, 2, 3)).to eq(42)
end

it "provides a default implementation" do
  dbl = double(:my_double)
  # No allow() statement needed here.
  expect(dbl.add(1, 2)).to eq(3)
end

Anonymous doubles can be used to quickly stub responses. Anonymous doubles aren't defined and take a string for their identifier (name).

it "is less verbose" do
  dbl = double("Anonymous", foo: 42)
  expect(dbl.foo).to eq(42)
end

Verifying doubles check that a type actually responds to methods that it has stubs for. The identifier for verifying doubles is the type it should check against. If the type doesn't exist yet (planned), then it behaves like a normal double.

# Method to test.
def string_length(str)
  str.size
end

def string_invalid(str)
  str.invalid
end

double String do
  stub size : Int32
  stub invalid : Int32
end

it "computes the string length" do
  dbl = double(String)
  allow(dbl).to receive(:size).and_return(42)
  expect(string_length(dbl)).to eq(42)
end

it "is doesn't have an #invalid method" do
  dbl = double(String)
  allow(dbl).to receive(:invalid).and_return(42)
  string_invalid(dbl) # Raises an error because String doesn't have an #invalid method.
end

All types of doubles can become a null double. Null doubles respond to every call. They return self by default. They can be created in two ways: null_double and double(NAME).as_null_object.

null_double :null do
  stub not_self { 42 }
end

let(dbl) { double(:null) }

it "can be stubbed" do
  expect(dbl.not_self).to eq(42)
end

it "returns itself" do
  expect(dbl.foo).to be(dbl)
end

double :thing do
end

it "returns itself when using .as_null_object" do
  dbl = double(:thing).as_null_object
  expect(dbl.foo).to be(dbl)
end

Mocks are used when the original type must be used. They have the same syntax as doubles, and must be defined before they can be used.

mock String do
  stub size : Int32
end

it "overrides the real value" do
  string = "foobar"
  allow(string).to receive(:size).and_return(42)
  expect(string.size).to eq(42)
end

The following receive() modifiers are allowed:

  • and_return
  • and_return(value)
  • and_return(*values)
  • and_raise(type)
  • and_raise(exception)
  • and_raise(message)
  • and_call_original
  • with(*args, **opts)

Spy functionality is enabled on all mocks and doubles.

double :my_double do
  stub :my_method { 42 }
end

it "calls #my_method" do
  dbl = double(:my_double)
  dbl.my_method
  expect(dbl).to have_received(:my_method)
end

it "calls #my_method (2)" do
  dbl = double(:my_double)
  expect(dbl).to receive(:my_method)
  dbl.my_method
end

The following receive() expectations are allowed:

  • with(*args, **opts)
  • once
  • twice
  • exactly(count).times
  • at_least(count).times
  • at_most(count).times
  • at_least_once
  • at_least_twice
  • at_most_once
  • at_most_twice
Edited by Mike Miller

Merge request reports