Skip to content
Mike Miller edited this page Dec 19, 2022 · 2 revisions

Mock (and stub) functionality can be "injected" into existing types. This may be necessary when a specific type is required (not a sub-class). Two examples of when this could be used are:

  1. Testing with a concrete struct (structs cannot be extended unless they're abstract).
  2. Testing calls to a type outside the boundaries of the shard or application that can't be changed (standard library or another shard).

Warning: This approach is not recommended. Injecting mock functionality into a type alters its behavior. It may behave differently between test and non-test code. Regular mocks and doubles should be used whenever possible. Especially avoid using this on foundational Crystal types such as String, Int32, and Array. It may cause unexpected behavior and stack overflows.

To add mocking to an existing type, use inject_mock.

struct MyStruct
  def something
    42
  end
end

inject_mock MyStruct

inject_mock works the same as defining a mock with mock (def_mock). It accepts the same arguments - the type name, any default stubs as keyword arguments, and a block for default stubs.

struct MyStruct
  def something
    42
  end

  def something_else(arg1, arg2)
    "#{arg1} #{arg2}"
  end
end

inject_mock MyStruct, something: 5 do
  stub def something_else(arg1, arg2)
    "foo bar"
  end
end

Instantiating a type like this should still be done with the mock (new_mock) method. However, this isn't required and isn't always possible. Instantiating the type normally (via new or other initializers) will produce an instance with mock and stub features. Keep in mind that the mock will revert to the default stubs between tests. In other words, the code defined in inject_mock is used everywhere, not just for the test context.

specify "creating a mocked type without `mock`" do
  inst = MyStruct.new
  expect(inst).to receive(:something).and_return(7)
  inst.something
end

it "reverts to default stub for other examples" do
  inst = mock(MyStruct)
  expect(inst.something).to eq(5) # Default stub used instead of original behavior.
end

After creating the mock, it behaves the same as normal mocks. allow, receive, expect, and have_received can all be used as expected.

Clone this wiki locally