liveBook · Manning

Spy

As explained in Unit Testing Principles, Practices, and Patterns

A spy is a manually written test double that records or verifies interactions with the system under test.

A spy is a manually written test double that records interactions with the system under test and verifies them afterward. It serves the same role as a mock, but avoids dependence on a mocking framework. section 5.1 section 9.1

Use spies when interaction verification is part of the test’s observable goal, especially at system edges.

  • Prefer a spy for classes at the system edge when reusable assertion code can make tests smaller and more readable. section 9.1
  • Target the last type before an unmanaged dependency when the exact external message structure requires backward-compatibility protection. section 9.1 section 9.3
  • Keep test assertions independent from production formatting and constants; otherwise the test can repeat the same defect and verify nothing. section 9.3
  • Use spies for outgoing interactions and use stubs for incoming data; do not assert incidental interactions with stubs. section 5.1 section 5.5

BusSpy for message verification

A BusSpy implements IBus, stores every sent message in a private list, and exposes fluent assertions such as ShouldSendNumberOfMessages and WithEmailChangedMessage. An integration test injects it through MessageBus, then chains assertions to verify one correctly formatted message. section 9.1

Listing 9.6. A spy (also known as a handwritten mock)
public interface IBus
{
    void Send(string message);
}

public class BusSpy : IBus
{
    private List<string> _sentMessages =          #1
        new List<string>();                       #1

    public void Send(string message)
    {
        _sentMessages.Add(message);               #1
    }

    public BusSpy ShouldSendNumberOfMessages(int number)
    {
        Assert.Equal(number, _sentMessages.Count);
        return this;
    }

    public BusSpy WithEmailChangedMessage(int userId, string newEmail)
    {
        string message = "Type: USER EMAIL CHANGED; " +
            $"Id: {userId}; " +
            $"NewEmail: {newEmail}";
        Assert.Contains(                          #2
            _sentMessages, x => x == message);    #2

        return this;
    }
}

Inspect the complete BusSpy implementation, including message recording and fluent verification. section 9.1

How Unit Testing Principles, Practices, and Patterns covers it

How deep it goes
Explains the mock distinction, edge-targeting rationale, assertion independence, and a fluent implementation, but not framework internals.
Examples
Demonstrates BusSpy recording message-bus calls and fluently checking count and formatted message content.
What it assumes
Assumes basic test doubles and integration testing, while distinguishing mocks from stubs and spies.
What you can do after
Builds a handwritten spy and applies it to readable integration-test interaction assertions.
How it connects
Connects spies with mocks, stubs, test doubles, unmanaged dependencies, integration tests, and observable behavior.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Unit Testing Principles, Practices, and Patterns. The section links point to the parts of the book it draws on.