Skip to content

Expert · Testing, tooling and Unity · topic 37 of 43 · 30 min

Test doubles: fakes, stubs and mocks

Explanation

Real code talks to slow or risky things: databases, SMS gateways, payment APIs, the clock. In unit tests you replace them with test doubles: simple stand-ins that implement the same interface. A stub returns fixed answers, a fake is a small working version (like an in-memory store), and a mock records calls so you can check them. Depending on interfaces, as you learned with dependency injection, is what makes this possible.

Why replace dependencies in tests?

Imagine testing an OrderService that charges a card and sends an SMS. Running it for real in every test would charge real money, send real messages, need the internet and take seconds. A unit test should check only OrderService's own logic. So you hand it pretend versions of the payment gateway and SMS sender.

Think of it like this: Test doubles are like the stunt doubles and dummy cars in a film shoot. The director does not crash a real Mercedes with the real hero inside to test a scene. A double looks the same from the camera's point of view (the interface) but is safe and cheap.

The kinds of test doubles

KindWhat it doesExample
DummyPassed in but never usedA logger nobody checks
StubReturns fixed, prepared answersA clock that always says 10:00 AM
FakeA simple working implementationAn in-memory list instead of a database
Spy / mockRecords how it was called so the test can checkAn SMS sender that remembers every message

Code designed for testing

csharp
public interface IPaymentGateway { Task<bool> ChargeAsync(string cardToken, decimal amount); }
public interface ISmsSender { Task SendAsync(string phone, string text); }

public class OrderService(IPaymentGateway payments, ISmsSender sms)
{
    public async Task<string> PlaceAsync(string phone, string cardToken, decimal amount)
    {
        if (amount <= 0) return "Invalid amount";
        bool paid = await payments.ChargeAsync(cardToken, amount);
        if (!paid) return "Payment failed";
        await sms.SendAsync(phone, $"Order confirmed: Rs {amount}");
        return "Confirmed";
    }
}
OrderService depends only on interfaces, received through its constructor, so tests can pass doubles in.

Hand-written doubles

csharp
class StubGateway(bool result) : IPaymentGateway
{
    public Task<bool> ChargeAsync(string cardToken, decimal amount) => Task.FromResult(result);
}

class SpySms : ISmsSender
{
    public List<(string Phone, string Text)> Sent { get; } = new();
    public Task SendAsync(string phone, string text)
    {
        Sent.Add((phone, text));
        return Task.CompletedTask;
    }
}
Task.FromResult and Task.CompletedTask give finished tasks without any real waiting.
csharp
public class OrderServiceTests
{
    [Fact]
    public async Task Place_PaymentSucceeds_SendsOneSms()
    {
        var sms = new SpySms();
        var service = new OrderService(new StubGateway(true), sms);

        string result = await service.PlaceAsync("98xxxxxx10", "tok_123", 499m);

        Assert.Equal("Confirmed", result);
        Assert.Single(sms.Sent);
        Assert.Contains("499", sms.Sent[0].Text);
    }

    [Fact]
    public async Task Place_PaymentFails_SendsNoSms()
    {
        var sms = new SpySms();
        var service = new OrderService(new StubGateway(false), sms);

        string result = await service.PlaceAsync("98xxxxxx10", "tok_123", 499m);

        Assert.Equal("Payment failed", result);
        Assert.Empty(sms.Sent);
    }
}
No money moves and no SMS is sent, yet both business rules are checked in milliseconds.

Controlling time

Code that uses DateTime.Now directly is hard to test ("is the shop open?" depends on when the test runs). Since .NET 8, the framework has an abstract TimeProvider class. Inject it, use TimeProvider.System in the real app, and pass a fake one in tests (Microsoft publishes a FakeTimeProvider in the Microsoft.Extensions.TimeProvider.Testing package), or write your own small IClock interface.

Mocking libraries

Writing doubles by hand is clear and fine for small interfaces. For bigger ones, teams use mocking libraries that create doubles at run time. Moq and NSubstitute are the most widely used in .NET; you will see them in many job descriptions. Their syntax differs, so learn whichever your project uses from its official documentation. The idea is always the same: set up what a method should return, and verify which calls were made.

Unit tests versus integration tests

Unit tests check one class with its dependencies replaced. Integration tests check several real parts together, for example the whole Web API with a real (test) database. ASP.NET Core supports this with WebApplicationFactory from the Microsoft.AspNetCore.Mvc.Testing package, which starts your app in memory so tests can send real HTTP requests to it. A healthy project has many fast unit tests and fewer, slower integration tests.

Common mistakes and fixes

  • Classes that create their own dependencies with new: they cannot be tested in isolation. Inject interfaces.
  • Mocking everything, including simple value objects and the class under test: tests become a copy of the implementation and break on every refactor. Double only the slow or risky edges.
  • Verifying every single call: test the outcome that matters (an SMS was sent with the amount), not every internal step.
  • Using static helpers for I/O (File, DateTime.Now) deep inside business logic: wrap them behind an interface at the edge.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Test doubles: fakes, stubs and mocks · 1/7

Test doubles

In this lesson you will replace slow or risky dependencies with test doubles.

Prefer a video by a teacher? Find free videos on “C# unit testing mocking dependencies fakes stubs xUnit Moq NSubstitute” on YouTube ↗

In real life: Testing a "shop is open" rule with a fixed clock

A kirana delivery app accepts orders only between 7 AM and 10 PM. To test both sides of the rule at any time of day, the service takes an IClock, and tests pass a stub that returns a chosen time.

csharp
public interface IClock { DateTime Now { get; } }
public class SystemClock : IClock { public DateTime Now => DateTime.Now; }

public class OrderWindow(IClock clock)
{
    public bool IsOpen() => clock.Now.Hour is >= 7 and < 22;
}

class FixedClock(DateTime time) : IClock { public DateTime Now => time; }

public class OrderWindowTests
{
    [Theory]
    [InlineData(6, false)]
    [InlineData(7, true)]
    [InlineData(21, true)]
    [InlineData(22, false)]
    public void IsOpen_ByHour(int hour, bool expected)
    {
        var window = new OrderWindow(new FixedClock(new DateTime(2026, 1, 15, hour, 0, 0)));
        Assert.Equal(expected, window.IsOpen());
    }
}
The real app registers SystemClock; tests use FixedClock. The tests pass at any hour you run them.

Practice (0/4 done)

Task 1 · Question

Which test double returns fixed, prepared answers?

Task 2 · Question

Why is a class that does new SmsGateway() inside its methods hard to unit test?

Task 3 · Question

Which is an integration test?

Task 4 · Practice

A ReminderService(IClock clock, ISmsSender sms) has SendIfDue(DateTime appointment, string phone): it sends an SMS only if the appointment is between 23 and 25 hours from now. Write the service, a FixedClock and a SpySms, and three tests: 24 hours away sends one SMS, 30 hours away sends none, 2 hours away sends none.

Quick quiz

Question 1

1.What do test doubles replace?

Question 2

2.Which design makes test doubles easy to use?

Question 3

3.Which .NET 8 type helps make time-dependent code testable?

Question 4

4.What is a risk of over-mocking?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What do test doubles replace?

Answer: Dependencies such as databases, gateways and clocks

You double the edges, not the code you are testing.

Which design makes test doubles easy to use?

Answer: Depending on interfaces passed in through the constructor

Constructor injection of interfaces.

Which .NET 8 type helps make time-dependent code testable?

Answer: TimeProvider

TimeProvider is an injectable abstraction over the clock.

What is a risk of over-mocking?

Answer: Tests mirror the implementation and break on every harmless refactor

Test behaviour and outcomes, not every internal call.

Why not mock every collaborator?Related

Answer: Tests then check call order, not behaviour, and break on every refactor

Over-mocking couples tests to implementation details.

From Test doubles and testable design

Your code sends real SMS messages. How should a unit test handle this?Related

Answer: Pass in a fake SMS sender that only records messages

A fake (a test double) stands in for the slow or risky part, and the test checks what the code asked it to do.

From Unit testing: checking one piece at a time

Which package is commonly used for mocks in Dart tests?Related

Answer: mocktail

mocktail creates configurable mocks.

From Unit tests

Which command runs local unit tests from the terminal?Related

Answer: ./gradlew test

connectedAndroidTest runs instrumented tests.

From Testing basics and the test pyramid

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Testing, tooling and Unity” before its exam: NuGet packages, solutions and project structure, Game development with Unity and C#.
  2. Module 9 exam: Testing, tooling and Unity · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Interviews, portfolio and careers. 43 topics left in the course before the final exam.

← Previous: Unit testing with xUnit