.NET Academy

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

DoubleWhat it doesExample
DummyFills a parameter; never usednull! or an empty object
StubReturns canned answersA gateway that always returns false
SpyRecords how it was called, for later assertionsA notifier that stores messages
MockPre-programmed with expectations, verifies interactions"Notify must be called once with X"
FakeA working, simplified implementationAn 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 verification

The 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.

Program.cs
Loading editor…

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 pass
  1. 1.What's a stub?

  2. 2.In Moq, what does `mock.Verify(n => n.Notify("Order of 10 confirmed"), Times.Once())` check?

  3. 3.When is a fake (a simplified working implementation) better than a mock?

  4. 4.What's a sign of over-mocking?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)