Expert · Testing, tooling and Unity · topic 37 of 43 · 30 min
Test doubles: fakes, stubs and mocks
Explanation
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.
The kinds of test doubles
| Kind | What it does | Example |
|---|---|---|
| Dummy | Passed in but never used | A logger nobody checks |
| Stub | Returns fixed, prepared answers | A clock that always says 10:00 AM |
| Fake | A simple working implementation | An in-memory list instead of a database |
| Spy / mock | Records how it was called so the test can check | An SMS sender that remembers every message |
Code designed for testing
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";
}
}Hand-written doubles
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;
}
}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);
}
}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
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.
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());
}
}Practice (0/4 done)
Which test double returns fixed, prepared answers?
Why is a class that does new SmsGateway() inside its methods hard to unit test?
Which is an integration test?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- NuGet packages, solutions and project structureTesting, tooling and Unity · same module
- Game development with Unity and C#Testing, tooling and Unity · same module
- Test doubles and testable designJavaScript Programming (language in depth) · Functional patterns and testing
- Unit testing: checking one piece at a timeBackend Development · Testing and API documentation
- Unit testsFlutter and Dart · Responsive design and testing
- Testing basics and the test pyramidKotlin and Android App Development · Testing Android apps
Questions people ask about this
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.
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.
Which package is commonly used for mocks in Dart tests?Related
Which command runs local unit tests from the terminal?Related
Answer: ./gradlew test
connectedAndroidTest runs instrumented tests.
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
- 2 more topics in “Testing, tooling and Unity” before its exam: NuGet packages, solutions and project structure, Game development with Unity and C#.
- Module 9 exam: Testing, tooling and Unity · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
- Then the next module, Interviews, portfolio and careers. 43 topics left in the course before the final exam.