// Testing · 03
Test Doubles
Stand-ins for collaborators, chosen by what the test checks.
How it fits together
- 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 readExpandCollapse
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.