Skip to content

// Testing · 03

Test Doubles

Real collaboratorleads toswap inTest doubleleads toIsolated test

Stand-ins for collaborators, chosen by what the test checks.

How it fits together

Test Doubles structure«client»Testarranges the doublesasserts on the mock onlyOrderServicePlaceOrder(order)system under testStub: product catalogPrice("sku-1") -> 42.00answers, never checkedMock: payment gatewayCharge(17, 42.00)verified: exactly once1 PlaceOrder2 price query3 charge command
uses / calls· numbered steps replay in order
  • Test uses / calls OrderService (1 PlaceOrder)
  • OrderService uses / calls Stub: product catalog (2 price query)
  • OrderService uses / calls Mock: payment gateway (3 charge command)

Spot it

  • Test needs a live database or payment API
  • Mock setup is longer than the test itself
  • Test breaks on a harmless internal refactor

It costs

Doubles keep tests fast and isolated, but they can drift from the real thing.

In code

public sealed class RecordingPaymentGateway : IPaymentGateway{    public List<(int OrderId, decimal Amount)> Charges { get; } = [];​    public Task ChargeAsync(int orderId, decimal amount, CancellationToken ct)    {        Charges.Add((orderId, amount));        return Task.CompletedTask;    }}​public sealed class OrderServiceTests{    [Fact]    public async Task PlaceOrder_KnownSku_ChargesCatalogPrice()    {        // Arrange        var gateway = new RecordingPaymentGateway();        var service = new OrderService(new FixedCatalog(42.00m), gateway);​        // Act        await service.PlaceOrderAsync(new Order(17, "sku-1"), CancellationToken.None);​        // Assert        gateway.Charges.ShouldBe(new[] { (17, 42.00m) });    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Gerard Meszaros named the family. A dummy fills a parameter and is never used. A stub returns canned answers. A fake is a working but simplified implementation, such as an in-memory repository. A spy records calls for inspection later. A mock is preset with expectations and fails when they are not met. The names matter less than the rule behind them: check a query by its result, and check a command by what it sent.

Prefer stubs and fakes for queries, and reserve mocks for outgoing commands such as sending an email or charging a card. Asserting every internal call couples the test to how the code is written, so a refactor breaks tests without changing behaviour. Do not mock what you do not own: wrap a third-party client behind your own interface and fake that. A double of a vendor type tests your fake, not the vendor.

A double can drift from the real system, so keep contract or integration tests for the boundary. When NOT to: value objects, pure functions and plain data need no doubles at all; use the real thing when it is cheap and deterministic.

Ask for it

  • dummy
  • stub
  • fake
  • spy
  • mock
  • test double

In OrderService tests, use a hand-written fake for IProductCatalog that returns 42.00 for sku-1, and a recording fake for IPaymentGateway. Assert the gateway recorded exactly one charge of 42.00 for order 17. Do not mock OrderService itself.