A mock and a stub are not the same thing wearing two different names — one checks what happened, the other just hands back an answer. Confusing them is one of the most common mistakes in testing.
Lesson 309's example quietly swapped a real database for an in-memory one, using WithWebHostBuilder. That's already a form of the idea this lesson makes explicit: sometimes a real dependency shouldn't be part of a test at all. A unit test (308) for an OrderService that sends a confirmation email shouldn't actually send a real email every time it runs — that's slow, it has side effects outside the test, and it makes the test dependent on an external mail server being reachable at all.
What you need instead is a stand-in: something that looks like the real dependency to the code under test, but behaves in a controlled, predictable way inside the test itself. This family of stand-ins is called test doubles, and the two most common — and most commonly confused — kinds are the mock and the stub.
In this lesson, you'll learn precisely what a mock and a stub each do (and how they genuinely differ), how to build them with Moq — the standard .NET mocking library — why mocking depends entirely on the interfaces and abstractions dependency injection already gave you, and the real risk of leaning on mocks too heavily.
A test double is a fake, controllable stand-in for a real dependency — used in a test so the thing actually being tested can run in isolation, without needing the real dependency to be present, slow, or even to exist yet. "Mock" is often used loosely to mean "any test double," but this lesson uses it precisely, because the precise version matters.
These two terms get blurred constantly in casual conversation, but they describe genuinely different things, and this distinction is worth holding onto carefully:
IDiscountRepository.GetDiscount() always returns 10m in this testmock.Verify(x => x.Send(...), Times.Once)IEmailSender.SendAsync(...) was actually called exactly once, with the right recipientMock<T> can serve either role) — the distinction is in how you use it in a given test, not in which class you imported.
This lesson uses Moq, the most widely used .NET mocking library and the one this course will use from here forward. NSubstitute is a well-regarded alternative with a slightly more fluent, less setup-heavy syntax — the underlying concepts in this lesson (mock vs. stub, setup, verify) apply identically regardless of which library a team happens to use.
Lesson 308 established that a genuine unit test has no I/O, runs in milliseconds, and produces the same result every time. The moment OrderService depends directly on a real SmtpEmailSender, a real payment gateway client, or DateTime.Now, none of that is true anymore — the test is now slow, network-dependent, and its outcome can depend on the current time or a third party's uptime. Mocking exists to cut that dependency out of the test entirely, while still letting OrderService's own logic run for real.
This only works because of a design decision that has nothing to do with testing on its face: depending on an interface (IEmailSender) rather than a concrete class (SmtpEmailSender), and having that dependency injected rather than constructed internally — exactly what lesson 124's dependency injection and lesson 235's Dependency Inversion Principle already taught you to do, for entirely different reasons at the time. Mocking is the payoff: because OrderService only knows about IEmailSender, a test can hand it any object that implements that interface — including one Moq builds automatically, purely for the test's own purposes.
OrderService's actual logic — validation, total calculation, deciding whether to send a confirmation — executes for real, exactly as it would in production.IEmailSender, IPaymentGateway, IClock — anything OrderService depends on through an interface is replaced with a Moq-generated stand-in for the duration of this one test.| Moq call | What it does |
|---|---|
new Mock<IEmailSender>() | Creates a dynamic, fake implementation of the interface |
.Setup(x => x.Method(...)) | Describes which call to configure a response for |
.Returns(value) | Programs the stubbed return value for that call — the "stub" role |
.Object | The actual fake instance to inject into the code under test |
.Verify(x => x.Method(...), Times.Once) | Asserts the call genuinely happened, this many times — the "mock" role |
public interface IDiscountRepository
{
decimal GetDiscountPercent(string customerId);
}
public interface IEmailSender
{
Task SendOrderConfirmationAsync(string customerId, decimal total);
}
public class OrderService
{
private readonly IDiscountRepository _discounts;
private readonly IEmailSender _emailSender;
public OrderService(IDiscountRepository discounts, IEmailSender emailSender)
{
_discounts = discounts;
_emailSender = emailSender;
}
public async Task<decimal> PlaceOrderAsync(string customerId, decimal subtotal)
{
var discountPercent = _discounts.GetDiscountPercent(customerId);
var total = subtotal - (subtotal * discountPercent / 100m);
await _emailSender.SendOrderConfirmationAsync(customerId, total);
return total;
}
}
public class OrderServiceTests
{
[Fact]
public async Task PlaceOrderAsync_CustomerWithDiscount_AppliesDiscountAndSendsConfirmation()
{
// Arrange
var discountMock = new Mock<IDiscountRepository>();
discountMock
.Setup(x => x.GetDiscountPercent("cust-1"))
.Returns(10m); // ← used as a STUB
var emailMock = new Mock<IEmailSender>(); // ← used as a MOCK
var service = new OrderService(discountMock.Object, emailMock.Object);
// Act
var total = await service.PlaceOrderAsync("cust-1", 100m);
// Assert
Assert.Equal(90m, total);
emailMock.Verify(
x => x.SendOrderConfirmationAsync("cust-1", 90m),
Times.Once);
}
}Meaning: discountMock plays a pure stub role — the test only cares what it returns, never whether or how it was called. emailMock plays a genuine mock role — the test's Verify call is a real assertion that the confirmation email logic actually ran, with the correct, discounted total. Both roles are backed by the same Mock<T> type; what makes one a stub and the other a mock is purely how the test uses it.
A checkout service that calls a real, third-party payment gateway is a textbook case for mocking: you don't own that dependency, it costs real money to actually call in a sandbox environment repeatedly, and its exact response format can't be relied on to hold still for your test suite's convenience.
[Fact]
public async Task Checkout_PaymentGatewayDeclines_OrderIsNotConfirmedAndNoEmailSent()
{
var paymentMock = new Mock<IPaymentGateway>();
paymentMock
.Setup(x => x.ChargeAsync(It.IsAny<string>(), It.IsAny<decimal>()))
.ReturnsAsync(PaymentResult.Declined);
var emailMock = new Mock<IEmailSender>();
var checkout = new CheckoutService(paymentMock.Object, emailMock.Object);
var result = await checkout.CompleteAsync("cust-1", 250m);
Assert.False(result.Success);
emailMock.Verify(
x => x.SendOrderConfirmationAsync(It.IsAny<string>(), It.IsAny<decimal>()),
Times.Never);
}Notice It.IsAny<decimal>() — Moq's argument matchers let a setup or verification apply regardless of the exact value passed, useful when the test cares that a call happened (or didn't) but not about every last argument. This test proves a genuinely important behavior — a declined charge must never trigger a confirmation email — entirely without a real payment gateway anywhere near the test.
A film crew doesn't send the lead actor off a real building to test a fall scene — they use a stunt double who looks the part from the camera's perspective and performs the fall safely and predictably. A stub is a stunt double who just needs to land where the script says — the crew doesn't care exactly how, they just need the scene to continue. A mock is a stunt double the director is specifically watching, stopwatch in hand, to confirm they hit three precise marks during the fall — the crew genuinely cares about the specific interaction, not just the end state. Both are stand-ins for the real thing; what differs is whether anyone's watching to verify exactly what the stand-in did.
IEmailSender — this is why Moq can only fake interfaces, or non-sealed classes with virtual members: it needs something it can genuinely override..Object — regardless of whether a .Setup(...) exists for it — is logged internally, including the exact arguments passed..Setup(...) calls in order, and returns whatever .Returns(...) specified for the first match; with nothing configured, it returns a default value (null, 0, an empty object, depending on the return type).Verify(...) call doesn't do anything at call time — it inspects the log of recorded invocations built during the test and throws a test-failing exception if the expected call, with the expected arguments and count, isn't found in it.It's genuinely common in casual conversation to hear "mock" used for both, and Moq's own class being named Mock<T> regardless of the role doesn't help. But the underlying distinction this lesson opened with is real: a stub exists to feed the code under test a canned value it needs to proceed; a mock exists to let the test assert that a specific interaction genuinely happened. Confusing the two leads directly to Common Mistake 1 below.
Moq's dynamic proxy generation works by overriding members — which means it can fake an interface freely, but for a concrete class it can only override virtual (or abstract) members. A sealed class, or a class with only non-virtual members, can't be mocked by Moq at all. This is, in practice, another reason to depend on interfaces for anything you intend to mock.
Adding a Verify(...) for every single internal call a method happens to make — including calls that are genuine implementation details, not part of the contract the test actually cares about. The test now breaks the moment the method is refactored internally, even if its externally observable behavior never changed at all.
Verify only the interactions that genuinely matter to the behavior being tested — "an email was sent," not "this exact private helper was called in this exact order." A test tied to implementation details is brittle in exactly the way lesson 308's Mistake 2 already warned about, now from the mocking side.
Mocking a simple, pure value object or a fast in-process helper class you wrote yourself, purely out of habit — this adds setup ceremony for a dependency that was never slow, external, or nondeterministic to begin with.
Reserve mocking for dependencies that are genuinely slow, external, or nondeterministic — a database, a third-party API, the system clock, a message queue. If the real thing is already fast and deterministic, just use it.
Trying to mock a sealed, third-party SDK class directly, and hitting the exact limitation described in Common Confusion above.
Wrap the third-party dependency behind your own interface (IPaymentGateway wrapping a vendor SDK's PaymentClient) — this is a direct application of the Dependency Inversion Principle (235), and it's precisely what makes the dependency mockable in the first place.
Verify specifically when a genuine, important interaction is the behavior under test — "did we actually attempt to charge the card.".Verify(...).You've learned the precise mock-vs-stub distinction, how Moq builds fakes, and where over-mocking goes wrong. Let's confirm it clicked.
1. A test sets up Mock<IDiscountRepository> to return 15m when called, and never checks whether or how many times it was called. What role is this test double playing?
Correct: B
Why B is correct: The role is determined by usage, not by which class built it — since the test never verifies the call happened, only uses the returned value, this is a stub role.
Why A is incorrect: Moq's Mock<T> class can serve either role — using it doesn't automatically make something "a mock" in the precise, behavior-verification sense this lesson defines.
Why C is incorrect: "Spy" wasn't part of this lesson's core distinction, and regardless, no verification is happening here at all — this is squarely a stub.
Why D is incorrect: Using a fake repository inside a fast, isolated test is exactly a unit test technique, not an integration test.
Reinforcement: Whether something is a stub or a mock depends on how the test uses it — verified interaction vs. just supplying a return value.
2. Why does mocking a dependency require that dependency to be exposed through an interface (or a class with virtual members)?
Correct: B
Why B is correct: This is exactly how Moq's dynamic proxy generation works — it needs a member it can override to intercept and record calls, which interfaces provide freely and non-sealed classes provide only through virtual/abstract members.
Why A is incorrect: This directly contradicts the "Common Confusion" section — a sealed class or one with only non-virtual members cannot be mocked by Moq.
Why C is incorrect: This has nothing to do with runtime performance — it's about whether Moq's proxy mechanism has something to override.
Why D is incorrect: Depending on interfaces is not only allowed, it's exactly what dependency injection (124) and the Dependency Inversion Principle (235) already taught — and it's precisely what enables mocking.
Reinforcement: Mockability is a direct consequence of depending on interfaces — another reason that habit pays off.
3. A test verifies not just that an order was placed successfully, but that a specific private helper method's internal call sequence matched exactly. After a harmless internal refactor that didn't change any observable behavior, the test breaks. What does this lesson call this?
Correct: B
Why B is correct: This is precisely Mistake 1 from the lesson — verifying implementation details rather than genuine behavior ties the test to internals that were never actually part of the contract worth protecting.
Why A is incorrect: A good test should fail when observable behavior breaks, not when harmless internals are refactored — that's exactly the brittleness this lesson warns against.
Why C is incorrect: Nothing here indicates a Moq defect — the test itself was written to verify the wrong thing.
Why D is incorrect: The fix isn't "use a stub instead" — it's to verify only the interactions that genuinely matter to the behavior under test, whether via a mock or otherwise.
Reinforcement: Verify behavior that matters to callers, not private implementation sequencing — that's what keeps tests resilient to safe refactors.
4. Which dependency is the strongest candidate for mocking in a unit test, based on this lesson's guidance?
Correct: B
Why B is correct: An external, slow, costly, and nondeterministic dependency you don't own is exactly the case mocking is built for — it keeps the test fast, free, and reliable.
Why A is incorrect: A fast, pure, in-process value object gains nothing from mocking — Mistake 2 warns against mocking dependencies that are already fast and deterministic.
Why C is incorrect: Private helpers within the class under test aren't external dependencies at all — they're part of the unit being tested, not something to substitute.
Why D is incorrect: A local variable isn't a dependency that can be injected or substituted — mocking doesn't apply to it.
Reinforcement: Reserve mocking for dependencies that are genuinely external, slow, or nondeterministic — not for anything already fast and yours.
You now know how to isolate a unit under test from its dependencies precisely — and precisely when a mock, a stub, or neither is the right call. Next: what happens when mocking a database entirely away costs you more confidence than it saves you time — Testcontainers.
dotnetmadeeasy.com — Learn C# and .NET, the right way.