Test doubles and mocking
Replace real dependencies with stubs, fakes and mocks to test code in isolation.
12 min read
You will learn
- Distinguish stubs, fakes, spies and mocks
- Write hand-rolled test doubles for interfaces
- Use a mocking library like Moq or NSubstitute
- Know when not to mock
Testing code that has dependencies
OrderService.PlaceOrder charges a card and sends a notification. Testing it against a real payment provider is slow, costs money, and can't easily simulate "card declined". Thanks to dependency injection (Track 8), we don't have to: OrderService depends on interfaces, so tests can pass test doubles — stand-ins that behave the way each test needs.
The kinds of test doubles
| Double | What it does | Example |
|---|---|---|
| Dummy | Fills a parameter; never used | null! or an empty object |
| Stub | Returns canned answers | A gateway that always returns false |
| Spy | Records how it was called, for later assertions | A notifier that stores messages |
| Mock | Pre-programmed with expectations, verifies interactions | "Notify must be called once with X" |
| Fake | A working, simplified implementation | An in-memory repository |
People often say "mock" for all of them — the distinction that matters is: are you feeding inputs (stub) or checking outputs and interactions (spy/mock)?
Hand-written doubles
For small interfaces, writing doubles by hand is simple and very readable:
class StubGateway(bool succeed) : IPaymentGateway
{
public bool Charge(decimal amount) => succeed;
}
class SpyNotifier : INotifier
{
public List<string> Messages { get; } = new();
public void Notify(string message) => Messages.Add(message);
}
[Fact]
public void PlaceOrder_PaymentDeclined_NotifiesAndRejects()
{
var notifier = new SpyNotifier();
var service = new OrderService(new StubGateway(succeed: false), notifier);
var result = service.PlaceOrder(42);
Assert.Equal("rejected", result);
Assert.Equal(["Payment of 42 failed"], notifier.Messages);
}Mocking libraries
For larger interfaces, libraries generate doubles at run time. With Moq:
var gateway = new Mock<IPaymentGateway>();
gateway.Setup(g => g.Charge(It.IsAny<decimal>())).Returns(true); // stub behavior
var notifier = new Mock<INotifier>();
var service = new OrderService(gateway.Object, notifier.Object);
service.PlaceOrder(10);
notifier.Verify(n => n.Notify("Order of 10 confirmed"), Times.Once()); // mock verificationThe same with NSubstitute, which many find more readable:
var gateway = Substitute.For<IPaymentGateway>();
gateway.Charge(Arg.Any<decimal>()).Returns(true);
var notifier = Substitute.For<INotifier>();
new OrderService(gateway, notifier).PlaceOrder(10);
notifier.Received(1).Notify("Order of 10 confirmed");Fakes for repositories
Data access is often best replaced by a fake — a tiny, real implementation:
class InMemoryOrderRepository : IOrderRepository
{
private readonly Dictionary<int, Order> orders = new();
public void Save(Order order) => orders[order.Id] = order;
public Order? Find(int id) => orders.GetValueOrDefault(id);
}Tests then assert on state ("the order was saved with status Paid") rather than on exact call sequences — far less brittle.
Don't over-mock
Mocks couple tests to how code works. If every internal class is mocked, every refactoring breaks dozens of tests even when behavior is unchanged. Guidelines:
- Mock boundaries you don't control or that are slow/non-deterministic: payment providers, email, HTTP APIs, the clock, randomness.
- Use real objects for your own simple domain classes.
- Prefer state verification (check results) over interaction verification (check calls) when you can.
- For the database, consider integration tests with a real database (next lesson) instead of mocking EF Core.
Key takeaways
- Test doubles replace real dependencies: stubs feed inputs, spies/mocks check interactions, fakes are lightweight implementations.
- Hand-written doubles are fine for small interfaces; Moq and NSubstitute generate them for bigger ones.
- Fakes (like in-memory repositories) make tests readable and less brittle.
- Mock boundaries, not everything — over-mocking makes tests fragile.
Exercise
OrderService is finished and works with any IPaymentGateway and INotifier. To test it, write two test doubles: StubGateway returns the result it was created with from Charge, and SpyNotifier records every message it receives in its Messages list.
C# runtime not loaded
Hint 1
StubGateway: store the constructor argument in a field and return it from Charge.
Hint 2
SpyNotifier.Notify just adds the message: Messages.Add(message).
Show solution
var spy = new SpyNotifier();
var service = new OrderService(new StubGateway(succeed: false), spy);
Console.WriteLine(service.PlaceOrder(25));
Console.WriteLine(string.Join(" | ", spy.Messages));
interface IPaymentGateway { bool Charge(decimal amount); }
interface INotifier { void Notify(string message); }
class OrderService(IPaymentGateway payments, INotifier notifier)
{
public string PlaceOrder(decimal total)
{
if (!payments.Charge(total))
{
notifier.Notify($"Payment of {total} failed");
return "rejected";
}
notifier.Notify($"Order of {total} confirmed");
return "confirmed";
}
}
class StubGateway(bool succeed) : IPaymentGateway
{
public bool Charge(decimal amount) => succeed;
}
class SpyNotifier : INotifier
{
public List<string> Messages { get; } = new();
public void Notify(string message) => Messages.Add(message);
}Quiz
4 questions · 3 correct to pass1.What's a stub?
2.In Moq, what does `mock.Verify(n => n.Notify("Order of 10 confirmed"), Times.Once())` check?
3.When is a fake (a simplified working implementation) better than a mock?
4.What's a sign of over-mocking?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)