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
