Skip to main content
ANVISoftware Solutions
Lesson 6 of 13Intermediate17 min

Mocking

By the end of this lesson

Replace a dependency without coupling the test to implementation detail.

A mocking library builds a test double for an interface at run time, so you configure behaviour in a few lines instead of writing a class.

Handled carefully, that is a genuine saving. Handled carelessly, it produces the most brittle tests in a codebase. The technique is easy; knowing what to assert afterwards is the part that takes judgement.

Two libraries dominate in .NET. The examples here use Moq; the ideas apply to both:

Moq
Configures behaviour with Setup and checks interactions with Verify. Explicit and widely used, so most .NET codebases you join will have it.
NSubstitute
A lighter syntax — Substitute.For<T>() and Returns — which reads more like plain C#. Same capabilities, same risks.
What both require
An interface or a virtual member to substitute. A sealed class with non-virtual methods cannot be replaced this way, which is one practical reason to depend on interfaces at boundaries.
Adding the package
Shell
dotnet add Ordering.Domain.Tests package Moq
  • One package in the test project only. A mocking library must never appear in the application's own dependencies.

Setting up a dependency

Stubbing a return value so the logic under test can run
C#
[Fact]
public void ApplyLoyaltyDiscount_ForEmployeeWithFiveOrders_ReducesTotalByFivePercent()
{
    // Arrange
    var existingOrders = new List<Order>
    {
        Order.Draft("E-1042"), Order.Draft("E-1042"), Order.Draft("E-1042"),
        Order.Draft("E-1042"), Order.Draft("E-1042"),
    };

    var repository = new Mock<IOrderRepository>();
    repository
        .Setup(r => r.FindByEmployee("E-1042"))
        .Returns(existingOrders);

    var service = new OrderService(repository.Object);
    Order order = Order.Draft("E-1042", total: 200m);

    // Act
    service.ApplyLoyaltyDiscount(order);

    // Assert
    Assert.Equal(190m, order.Total);
}
  • new Mock<IOrderRepository>() creates the double. Every member starts out doing nothing and returning the default for its type — null for a reference, zero for a number.
  • Setup(...).Returns(...) makes one call answer a fixed way. This is a stub: it supplies the input the logic needs, and nothing more.
  • repository.Object is the actual IOrderRepository you pass to the service. The Mock wrapper is the control panel; Object is the thing under the panel.
  • The assertion is on the outcome — the total on the order. Not on the repository. That is what keeps this test about behaviour.
  • Use It.IsAny<string>() in place of "E-1042" when the argument is genuinely irrelevant. Matching the real argument is more precise, so prefer it when the value matters.

Verifying an interaction, when there is nothing else to check

Some dependencies have no observable result inside your system. A notification service sends an email and returns nothing. There is no state to inspect, so the only thing a test can check is that the request was made, with the right content.

This is the case where verification is the right tool rather than a shortcut.

Verifying the content of an outbound request
C#
[Fact]
public void Submit_WhenOrderIsValid_RequestsConfirmationForThatEmployee()
{
    // Arrange
    var repository = new InMemoryOrderRepository();
    var notifier = new Mock<INotificationService>();
    var service = new OrderService(repository, notifier.Object);
    Order order = service.CreateOrder("E-1042", quantity: 2, unitPrice: 30m);

    // Act
    service.Submit(order.Id);

    // Assert
    notifier.Verify(n => n.SendOrderConfirmation(
        It.Is<OrderConfirmation>(c => c.EmployeeId == "E-1042" && c.Total == 60m)));
}
  • Verify asserts that a matching call happened. Without a Times argument it requires at least one, which is the looser and more robust default.
  • It.Is<T>(predicate) checks the argument rather than only the fact of the call. The confirmation is addressed to the right employee for the right amount — that is behaviour a user would notice.
  • The repository here is a fake, not a mock. Mix freely: use the kind of double that suits each dependency in the same test.
  • Nothing in this assertion says how many times the notifier was called, or in what order relative to the save. Both are implementation choices.

The same intention, expressed two ways, against a refactor that batches saves:

 Interaction assertionOutcome assertion
The test saysrepository.Verify(r => r.Save(order), Times.Once)Assert.Equal(OrderStatus.Submitted, fake.Find(order.Id)!.Status)
After batching two saves into oneFailsPasses
If the order is never stored at allFailsFails
What the failure meansThe call pattern changed — investigate whether that mattersThe order was not stored correctly — a real defect

Signals that mocking has gone too far

Any of these suggests stepping back and reconsidering the test or the design:

  • Four or more mocks in one arrange block — the class under test probably has too many collaborators
  • A Setup for a call the test does not care about, added only to stop a null reference
  • Verifying calls on a dependency whose result you could have asserted on instead
  • A test you cannot read without knowing the implementation, because it is a transcript of it
  • Mocking a type you own and could have replaced with a small fake
  • Mocking something with no meaningful behaviour, such as a value object or a logger

Summary

  • A mocking library substitutes an interface at run time, so you configure behaviour instead of writing a class
  • Setup supplies the inputs the logic needs; that use is a stub and is generally safe
  • Unconfigured members return type defaults silently, which produces confusing failures
  • Verifying call counts asserts on implementation and breaks on refactors that change nothing observable
  • Verify content for outbound requests with no observable result; otherwise assert on outcomes using a fake

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Replace a verification with an outcome

You have this test: it creates a Mock<IOrderRepository>, calls service.Submit, and asserts repository.Verify(r => r.Save(It.IsAny<Order>()), Times.Once).

Rewrite it using the in-memory fake so that it asserts on the stored result instead. Then change Submit to save twice and confirm your new test still passes.

Show solution

The rewritten test asks the question that matters: after submitting, is the stored order in the submitted state? A double save is wasteful but not a behaviour change, so the test staying green is correct.

If duplicate saves were genuinely a problem — because each one charges a card or sends a message — then the count is behaviour and asserting on it is right. The rule is not "never verify"; it is "verify only what a user or another system would notice".

C#
[Fact]
public void Submit_WhenOrderIsDraft_StoresItAsSubmitted()
{
    var repository = new InMemoryOrderRepository();
    var service = new OrderService(repository);
    Order order = service.CreateOrder("E-1042", quantity: 2, unitPrice: 30m);

    service.Submit(order.Id);

    Order stored = repository.Find(order.Id)!;
    Assert.Equal(OrderStatus.Submitted, stored.Status);
    Assert.Equal(60m, stored.Total);
}

Think about it

Five mocks

A colleague's test arranges five mocks and eleven Setup calls before a single line of action. All the tests pass. What would you say in review?

Show solution

The tests passing is not the issue. The setup is describing a class with five collaborators, and that is the finding worth raising — the test is a fair report on the design.

Two directions to suggest. Split the class so each piece has one or two dependencies, which makes both the code and the tests simpler. Or, if the class is genuinely an orchestrator, test the pieces it coordinates individually and cover the orchestration with a smaller number of integration tests instead.

Worth saying out loud in review: hard-to-write tests are usually information about the code, not a reason to write worse tests.

Saved in this browser only.