Advanced Track
Mocking & Test Doubles
By the end of this lesson you'll be able to isolate the class you're testing from its dependencies — databases, email, web APIs — by replacing them with test doubles. You'll hand-roll your own fakes, then use Moq to do it in two lines, and you'll know the difference between testing state and testing interactions — plus when not to mock at all.
What You'll Learn
- Name the five test doubles: dummy, stub, fake, mock, and spy
- Hand-roll a fake that records how its methods were called
- Inject dependencies so they can be swapped for doubles in a test
- Use Moq: new Mock, Setup/Returns, and Verify with Times
- Tell state testing (check the result) from interaction testing (check the calls)
- Recognise when NOT to mock — and avoid brittle, over-mocked tests
[Fact] with an assertion, because mocking is what you reach for the moment a class under test depends on something you don't want a unit test to actually run.💡 Real-World Analogy
Car makers test safety with a crash-test dummy, not a real person. The dummy stands in for a human: it's the right shape and weight to make the test realistic, but it's controlled and disposable, so you can run the crash a hundred times without anyone getting hurt. A test double is exactly that — a stand-in for a real dependency. Your real email service sends actual emails and your real database is slow and shared; in a test you swap them for a controlled stand-in that you can drive, inspect, and throw away. The dummy isn't the point of the test — the car's behaviour is. Likewise, the double isn't what you're testing; it's there so you can test your own code in isolation.
The Test Double Taxonomy
"Mock" gets used as a catch-all, but there are really five kinds of test double (the names come from Gerard Meszaros via Martin Fowler). They differ in how much they do and what you check. You won't always say the word out loud, but knowing which one you're building keeps your tests honest.
| Double | What it does | You use it to… |
|---|---|---|
| dummy | Nothing — just fills a parameter | Satisfy a signature you don't actually call |
| stub | Returns canned, fixed answers | Make the code under test deterministic |
| fake | A working but lightweight implementation | Replace a real DB with an in-memory one |
| mock | Pre-programmed with expectations | Assert the right calls were made (interaction) |
| spy | Records how it was called | Inspect call counts and arguments afterwards |
The everyday split: a stub feeds data in (you assert on the result — state testing); a mock/spy records calls going out (you assert on the calls — interaction testing). A fake is a real, simplified implementation you could almost ship. Libraries like Moq blur the lines — one Mock<T> can act as a stub and a mock.
1. Hand-Rolling a Fake That Records Calls
Before any framework, understand what a double actually is — just a class that implements the same interface as the real thing. The key trick that makes it swappable is dependency injection: the class under test takes its dependency through its constructor rather than creating it, so in a test you hand it a double instead. This first double is a spy — it records how many times Send was called so the test can check the interaction. Read it, run it, then you'll build one.
Worked example: a fake IEmailService that counts calls
Read every comment, run it, and watch the spy report two Send calls.
using System;
using System.Collections.Generic;
// The dependency we want to keep OUT of our test — in real life this would
// actually send email over the network. We never want a unit test to do that.
interface IEmailService
{
void Send(string to, string subject);
}
// A hand-rolled FAKE (a "spy"): it implements the interface but instead of
// sending anything, it just RECORDS what it was asked to do. No framework needed.
class FakeEmailService : IEmailService
{
public int SendCount
...Your turn. The fake below is missing the bits that make it record anything. Fill in the three ___ blanks so the test can count the emails, then run it.
🎯 Your turn: make the fake count Send calls
Start the counter, increment it on each call, and assert the count is 3.
using System;
// 🎯 YOUR TURN — finish the FAKE so the test can count Send calls.
interface IEmailService
{
void Send(string to, string subject);
}
class FakeEmailService : IEmailService
{
// 1) Add a counter the test can read later, starting at 0.
public int SendCount = ___; // 👉 start the count at 0
public void Send(string to, string subject)
{
// 2) Record this call by increasing the counter by one.
___; // 👉 increment
...2. Stubs — Canned Data for Deterministic Tests
A stub solves a different problem from a spy. Where a spy watches calls going out, a stub feeds data in: it returns the same fixed value every time, so the method under test produces the same result on every run. That's what makes a test deterministic — it never depends on what's in a real database or what a remote API happens to return today. Here the stub always reports a price of £10, so you can assert the discount maths exactly. Fill in the two blanks.
🎯 Your turn: a stub returning canned data
Return a fixed price from the stub and assert the discounted total.
using System;
// A STUB feeds CANNED data to the code under test so it behaves the same way
// every run — no real database, no randomness. We test the LOGIC, deterministically.
interface IPriceProvider
{
decimal GetPrice(string sku); // real version would hit a database
}
// 🎯 YOUR TURN — finish the stub so GetPrice always returns a fixed value.
class StubPriceProvider : IPriceProvider
{
public decimal GetPrice(string sku)
{
// 1) Ignore the sku and always return the
...3. Moq — Setup, Returns & Verify
Hand-rolling a double for every interface gets tedious. Moq is the most popular .NET mocking library: new Mock<IFoo>() generates a fake implementation at runtime, and you reach the fake object via .Object. You stub a return value with .Setup(x => x.Method(arg)).Returns(value), and you assert an interaction happened with .Verify(x => x.Method(arg), Times.Once). The next two examples are real test code — read them and note the expected outcome in each comment (the runnable exercises above already let you practise the underlying idea by hand).
Worked example: Moq Setup/Returns and Verify
One test stubs a return value (state); the other verifies the calls made (interaction).
using Moq;
using Xunit;
public interface IUserRepository
{
User? GetById(int id);
void Add(User user);
}
public interface IEmailService
{
void SendWelcome(string email);
}
public class User { public int Id; public string Name = ""; public string Email = ""; }
// Service under test — depends only on interfaces (so they can be mocked).
public class UserService
{
private readonly IUserRepository _repo;
private readonly IEmailService _email;
public UserService(IUserReposi
...4. Verifying Calls: Times, Matchers & NSubstitute
Verify takes a Times argument — Times.Once, Times.Never, Times.Exactly(n) — to assert how many times a call happened. When you don't care about the exact argument, match any value with It.IsAny<T>() or a predicate with It.Is<T>(...). NSubstitute is a popular alternative with lighter syntax — no .Object, no Setup keyword; you stub with .Returns(...) and verify with .Received(n). Pick one per project and stay consistent.
Worked example: Times, It.IsAny, and NSubstitute
Compare Moq's Verify/Times/It.IsAny with NSubstitute's Returns/Received.
using Moq;
using Xunit;
public class VerifyAndMatchersTests
{
[Fact]
public void Verify_CountsAndArgumentMatchers()
{
var email = new Mock<IEmailService>();
// Drive the mock a couple of times.
email.Object.SendWelcome("a@test.com");
email.Object.SendWelcome("b@test.com");
// Times.Exactly(n) — assert the EXACT number of calls.
// It.IsAny<string>() — match ANY string argument.
email.Verify(e => e.SendWelcome(It.IsAny<string>
...🔎 Deep Dive: state testing vs interaction testing
State testing asks "after I run this, is the result right?" You stub the inputs and assert on the value that comes back. It's robust: it doesn't care how the answer was reached, so you can refactor freely. Prefer it whenever you can.
Interaction testing asks "did my code call the right collaborator the right way?" You Verify the calls. It's essential when the whole point of a method is a side effect — sending an email, writing to a queue — where there's no return value to inspect. But it couples your test to how the code works, so use it only for genuine, meaningful side effects.
// STATE: stub the input, assert the output.
repo.Setup(r => r.GetById(1)).Returns(new User { Name = "Al" });
Assert.Equal("Al", service.GetName(1));
// INTERACTION: drive the code, assert the call happened.
service.Register("Bob", "bob@x.com");
email.Verify(e => e.SendWelcome("bob@x.com"), Times.Once);Rule of thumb: verify a side effect, but assert a return value. If you find yourself verifying every call a method makes, you're probably re-stating the implementation in your test — that's the brittleness trap below.
🔎 Deep Dive: when NOT to mock
Mocking is a tool, not a default. Reach for it only when the real thing is slow, non-deterministic, has side effects, or isn't built yet. Otherwise, the real object makes a better test. Skip the mock when:
- • The dependency is pure and fast — a calculator, a formatter, a value object. Use the real one; a mock just adds noise.
- • You'd be mocking a type you don't own (a third-party SDK,
HttpClient,DbContext). Wrap it in your own thin interface and mock that instead. - • You're tempted to mock the class under test (or part of it). If you have to, the class is doing too much — split it.
- • An in-memory fake reads better than a pile of
Setuplines — e.g. a realList<T>-backed repository.
For things like databases, an integration test against a real (or in-memory) database often gives more confidence than a mock that just parrots back what you told it.
Pro Tips
- 💡 Mock interfaces, not concrete classes: Moq can only override
virtual/abstractmembers, so a sealed concrete class can't be mocked. Depend on an interface and you're free. - 💡 Prefer a stub to a mock when you can assert on a return value. Verifying calls couples the test to the implementation; asserting state doesn't.
- 💡 Only own-types get mocked. Wrap third-party SDKs and
HttpClientbehind your own interface, then mock the interface — never the library. - 💡 One mock per dependency, not per call. If a single test sets up five mocks, the class under test probably has too many responsibilities.
- 💡 Use
MockBehavior.Strictsparingly:new Mock<IRepo>(MockBehavior.Strict)throws on any unexpected call. It catches accidents but makes tests fragile, so reserve it for when call-completeness really matters.
Common Errors (and the fix)
- Over-mocking: a test with six mocks and ten
Setuplines tests your mocks, not your code. Mock only the dependencies that are slow or have side effects; use real objects for the rest. - Mocking what you don't own: mocking a third-party SDK or
HttpClientdirectly is brittle — the library changes and your tests break for no real reason. Wrap it in your own interface and mock that. - Brittle interaction tests: verifying every internal call means any refactor breaks the test even though behaviour is unchanged. Verify only the meaningful side effects (the email was sent); assert the return value for everything else.
- Mocking concrete classes: "System.NotSupportedException: Unsupported expression… non-overridable members may not be used in setup" means you tried to
Setupa non-virtualmethod on a concrete class. Mock an interface (or make the membervirtual). - Forgetting
.Object: passing theMock<IFoo>itself where anIFoois expected won't compile. Passmock.Object— the generated fake — into your service.
📋 Quick Reference
| Task | Moq | NSubstitute |
|---|---|---|
| Create a double | new Mock<IFoo>() | Substitute.For<IFoo>() |
| Get the fake object | mock.Object | (the substitute itself) |
| Stub a return | .Setup(x => x.M()).Returns(v) | sub.M().Returns(v) |
| Match any arg | It.IsAny<int>() | Arg.Any<int>() |
| Verify once | .Verify(x => x.M(), Times.Once) | sub.Received(1).M() |
| Verify never | .Verify(x => x.M(), Times.Never) | sub.DidNotReceive().M() |
Both libraries do the same job. It.IsAny<T>()/Arg.Any<T>() match any argument; It.Is<T>(predicate)/Arg.Is<T>(predicate) match by a condition.
Frequently Asked Questions
Q: What's the real difference between a stub and a mock?
A stub feeds canned data in so the code under test is deterministic — you then assert on the result. A mock records the calls made to it so you can Verify the right interactions happened. Stub = state testing; mock = interaction testing. Moq can do both from one Mock<T>.
Q: Why can't I mock a concrete class?
Moq works by generating a subclass and overriding members, so it can only override virtual or abstract ones. A normal concrete method isn't overridable, so the setup throws. Depend on an interface (or mark members virtual) and the problem disappears.
Q: Should I mock HttpClient or my DbContext?
No — those are types you don't own, and mocking them directly is painful and brittle. Wrap them behind a small interface of your own (IWeatherApi, IUserRepository) and mock that. For a database, an in-memory or integration test often gives more confidence than any mock.
Q: My tests break every time I refactor — what am I doing wrong?
You're likely over-using interaction testing — verifying internal calls that are implementation detail. Verify only the meaningful side effects and assert on return values for the rest. Tests should track behaviour, not the exact sequence of method calls.
Q: Do I even need a framework, or can I write fakes by hand?
You can — and for simple interfaces a hand-rolled fake is often clearer (you saw two above). Frameworks like Moq pay off when an interface is large or you need flexible argument matching and call counts without writing all that boilerplate yourself.
Mini-Challenge: Fake Repository
No blanks this time — just a brief and an outline. Build an in-memory FakeRepository that records saved items, inject it into NoteService, add two notes, and assert your fake recorded exactly two with the right first item. Run it and check your output against the expected lines in the comments.
🎯 Mini-Challenge: test a service with a fake IRepository
Write the fake, inject it, exercise the service, and assert the recorded calls.
using System;
using System.Collections.Generic;
// 🎯 MINI-CHALLENGE: test a service using a hand-rolled fake repository.
//
// 1. Implement IRepository with an in-memory fake:
// - back it with a List<string> field called Saved
// - Save(item) should add the item to that list
// - Count() should return how many items have been saved
// 2. In Main, inject your fake into the NoteService below.
// 3. Call service.AddNote("buy milk") and service.AddNote("call mum").
// 4. Assert the fak
...🎉 Lesson Complete
- ✅ Five test doubles: dummy, stub, fake, mock, spy
- ✅ A double is just a class implementing the same interface, swapped in via dependency injection
- ✅ Moq:
new Mock<IFoo>(),.Setup(...).Returns(...),.Verify(..., Times.Once),.Object - ✅ Stubs feed data in (state testing); mocks/spies record calls (interaction testing)
- ✅ When not to mock: pure/fast types, types you don't own, the class under test itself
- ✅ Avoid over-mocking and brittle interaction tests — verify side effects, assert return values
- ✅ Next lesson: Test-Driven Development — write the failing test first, then the code
Sign up for free to track which lessons you've completed and get learning reminders.