Skip to content

Latest commit

 

History

History
321 lines (248 loc) · 9.2 KB

stubs.md

File metadata and controls

321 lines (248 loc) · 9.2 KB

Stubs

A stub defines replacement behavior of a method. Stubs are commonly used to redefine behavior of methods for mocks and doubles. There are different types of stubs for various behaviors.

Fluent language

Stubs are normally created with the fluent language (DSL) methods. The language to create a stub typically starts with receive. The receive method takes the name of the method to stub as a symbol.

For instance, with the "can" syntax used by Spec:

private double TestDouble, value: 0

it "creates a stub" do
  my_double = TestDouble.new
  my_double.can receive(:value)
  #   ^      ^     ^ Starts the creation of a stub.
  #   |      + Accepts a stub and applies it to `my_double`.
  #   + Object to apply the stub to.
end

And similarly with the "allow" syntax used by Spectator:

private double TestDouble, value: 0

it "creates a stub" do
  my_double = TestDouble.new
  allow(my_double).to receive(:value)
  # ^       ^      ^     ^ Starts the creation of a stub.
  # |       |      + Accepts a stub and applies it to `my_double`.
  # |       + Object to apply the stub to.
  # + Wraps an object to be stubbed.
end

When receive is used by itself, it will create a stub that returns nil. Modifiers are typically added after the receive call to specify the stub's behavior.

and_return Modifier

The and_return modifier is the simplest stub modifier. It changes the return value.

private double TestDouble, value: 0

it "modifies the return value" do
  my_double = TestDouble.new
  my_double.can receive(:value).and_return(42)
  my_double.value.should eq(42)
end

The type of value must match that of the original method. Otherwise, TypeCastError is raised indicating the stub's value couldn't be returned.

private double TestDouble, value: 0 # Return type inferred to be `Int32`

it "cannot use a different type" do
  my_double = TestDouble.new
  my_double.can receive(:value).and_return("not a number") # Wrong!
  my_double.value.should eq("not a number") # Error!
end

In the example above, the following error is given at runtime:

Attempted to return "not a number" (String) from stub, but method `value` expects type Int32 (TypeCastError)

The and_return modifier can accept a list of arguments. Each call to the stub returns the next argument.

private double TestDouble, value: 0

it "returns multiple values" do
  my_double = TestDouble.new
  my_double.can receive(:value).and_return(1, 2, 3)
  my_double.value.should eq(1)
  my_double.value.should eq(2)
  my_double.value.should eq(3)
end

After all of the arguments are exhausted, the last one is returned for additional calls.

private double TestDouble, value: 0

it "returns multiple values" do
  my_double = TestDouble.new
  my_double.can receive(:value).and_return(1, 2, 3)
  my_double.value.should eq(1)
  my_double.value.should eq(2)
  my_double.value.should eq(3)

  my_double.value.should eq(3)
  my_double.value.should eq(3)
end

Using and_return with no arguments changes the stub to return nil.

Block Modifier

The receive method can accept a block. That block is executed when the stub is invoked. The value returned by the block will be returned by the stub and the method. This can be used to define dynamic behavior for stubs.

private double TestDouble, add_item: nil

it "executes a block" do
  list = [] of Int32
  my_double = TestDouble.new
  my_double.can receive(:add_item) { list << list.size }
  3.times { my_double.add_item }
  list.should eq([0, 1, 2])
end

and_raise Modifier

The and_raise modifier causes the stub to raise an exception when it is invoked. This modifier accepts various arguments, all of which are used to specify the exception to raise. The variants are:

.and_raise(String) - Raise a RuntimeError with the message specified.

private double TestDouble, oof: nil

it "raises an error" do
  my_double = TestDouble.new
  my_double.can receive(:oof).and_raise("Something went wrong")
  expect_raises(RuntimeError, "Something went wrong") { my_double.oof }
end

.and_raise(Exception) - Specifies the exception object to raise.

it "raises an error" do
  exception = DivisionByZeroError.new("You broke the universe")
  my_double = TestDouble.new
  my_double.can receive(:oof).and_raise(exception)
  expect_raises(DivisionByZeroError, "You broke the universe") do
    my_double.oof
  end
end

.and_raise(Exception.class, *args, **kwargs) - Creates an exception of type Exception by calling new on it and forwarding the rest of the arguments.

it "raises an error" do
  my_double = TestDouble.new
  my_double.can receive(:oof).and_raise(NilAssertionError, "Value can't be nil")
  expect_raises(NilAssertionError, "Value can't be nil") { my_double.oof }
end

and_call_original Modifier

The and_call_original modifier causes the stub to call the original implementation of the method. The stub effectively becomes a passthrough. This is useful for mocks where, by default, it prevents calling the original methods.

private class TestClass
  getter value = 42
end

mock MockTestClass < TestClass

it "calls the original method" do
  my_mock = MockTestClass.new

  # By default, the mock's method cannot be called.
  expect_raises(Mocks::UnexpectedMessage, /value/) { my_mock.value }

  # Allow the method to be called.
  my_mock.can receive(:value).and_call_original
  my_mock.value.should eq(42)
end

It can be combined with the with modifier to call the original method for only specific arguments.

private class TestClass
  def stringify(value)
    value.to_s
  end
end

mock MockTestClass < TestClass

it "calls the original method" do
  my_mock = MockTestClass.new
  my_mock.can receive(:stringify).and_return("foo")
  my_mock.can receive(:stringify).with(42).and_call_original
  my_mock.stringify(:xyz).should eq("foo")
  my_mock.stringify(42).should eq("42")
end

with Modifier

The with modifier changes the arguments that must be matched to trigger the stub. The default stub is used if none of the other argument patterns match.

private double TestDouble, do_something: 0

it "can change the expected arguments" do
  my_double = TestDouble.new
  my_double.can receive(:do_something).with(1).and_return(42)
  my_double.do_something(1).should eq(42) # Matches the stub above.
  my_double.do_something(2).should eq(0)  # Uses the default stub.
end

All arguments are compared with the case-equality operator (===).

private double TestDouble, do_something: 0

it "can pattern match arguments" do
  my_double = TestDouble.new
  my_double.can receive(:do_something).with(/foo/).and_return(42)
  my_double.do_something("foobar").should eq(42)
  my_double.do_something("baz").should eq(0)
end

The with modifier accepts multiple arguments. They will be matched against the positional arguments in order.

private double TestDouble, do_something: 0

it "can match multiple arguments" do
  my_double = TestDouble.new
  my_double.can receive(:do_something).with(1, /foo/).and_return(42)
  my_double.do_something(1, "foobar").should eq(42)
  my_double.do_something(0, "foobar").should eq(0) # First argument doesn't match.
  my_double.do_something(1, "baz").should eq(0)    # Second argument doesn't match.
end

Keyword arguments can also be matched by using key-value pairs as arguments in with.

private double TestDouble, do_something: 0

it "can match keyword arguments" do
  my_double = TestDouble.new
  my_double.can receive(:do_something).with(arg: /foo/).and_return(42)
  my_double.do_something(arg: "foobar").should eq(42)
  my_double.do_something(arg: "baz").should eq(0)    # Value doesn't match.
  my_double.do_something(foo: "foobar").should eq(0) # Key doesn't match.
end

Positional arguments and keyword arguments can be mixed.

private double TestDouble, do_something: 0

it "can match positional and keyword arguments" do
  my_double = TestDouble.new
  my_double.can receive(:do_something).with(1, arg: /foo/).and_return(42)
  my_double.do_something(1, arg: "foobar").should eq(42)
  my_double.do_something(0, arg: "foobar").should eq(0) # Positional argument doesn't match.
  my_double.do_something(1, arg: "baz").should eq(0)    # Keyword argument doesn't match.
end

Positional arguments can be matched with keyword arguments. This is more explicit. It may be useful to use the anything keyword to match the other arguments.

private double LoggerDouble do
  def log(level, message)
    false
  end
end

it "can match positional arguments with keyword arguments" do
  my_double = LoggerDouble.new
  my_double.can receive(:log).with(level: :warn, message: anything).and_return(true)
  my_double.log(:warn, "oof").should be_true
  my_double.log(:info, "foo").should be_false
end

The with modifier can take a block, which is used for the return value.

private double TestDouble, do_something: 0

it "accepts a block" do
  my_double = TestDouble.new
  my_double.can receive(:do_something).with(1) { 42 }
  my_double.do_something(1).should eq(42)
end