Test Doubles and Dependency Seams

Depend on an interface and a test can supply its own version: hand-written fakes, mocking libraries, and when to use each.

SyntaxMock<IEmailSender> mock = new Mock<IEmailSender>(); IEmailSender doubleInstance = mock.Object;

Code under test usually depends on something you do not want a test to touch: a mail server, a payment API, a database. A test double is a stand-in for that thing, written so the test is in control. To put one in place you need a seam: a point where the dependency arrives from outside the class instead of being created inside it.

The seam is an interface plus a constructor parameter

An interface is a job description. It lists the methods something must provide and says nothing about how. Declare the dependency as an interface and accept it as a constructor parameter. Production passes the real implementation; the test passes its own. If the class writes new SmtpEmailSender() in its own body, there is no seam and no test can avoid sending mail. Passing dependencies in by hand like this is all you need here; Stage 5 shows the container that does the passing for you in a web application.

Hand-written or library

A hand-written double is a small class implementing the interface. A stub answers questions with fixed values. A fake is a working but simplified implementation, such as an in-memory store. A mock records the calls it received so the test can check them.

A mocking library such as Moq or NSubstitute generates that class at run time, so one line replaces a file. The cost is that a mock set up for specific arguments silently returns a default for every other argument, and tests that verify long sequences of calls break whenever you refactor. Hand-written doubles stay readable longer; libraries win when the interface is wide or you need to assert on calls.

Example

Example · csharp
// A hand-written test double, with no test framework involved.

RecordingEmailSender sender = new RecordingEmailSender();
SignupService service = new SignupService(sender);

service.Register("[email protected]");
service.Register("[email protected]");

Console.WriteLine($"emails sent: {sender.Sent.Count}");
Console.WriteLine($"first: {sender.Sent[0]}");

// The job description. Both implementations below must satisfy it.
public interface IEmailSender
{
    void Send(string to, string subject);
}

// Used in production. Would open a network connection.
public class SmtpEmailSender : IEmailSender
{
    public void Send(string to, string subject)
    {
        throw new NotSupportedException("Not wired up in this example.");
    }
}

// The double: records the calls instead of making them.
public class RecordingEmailSender : IEmailSender
{
    public List<string> Sent { get; } = new();

    public void Send(string to, string subject)
    {
        Sent.Add($"{to} / {subject}");
    }
}

public class SignupService
{
    private readonly IEmailSender _email;

    // The seam: the dependency comes in from outside.
    public SignupService(IEmailSender email)
    {
        _email = email;
    }

    public void Register(string address)
    {
        _email.Send(address, "Welcome");
    }
}

// Output:
// emails sent: 2
// first: [email protected] / Welcome

When to use it

  • Test the code that sends a welcome email without sending one.
  • Make a calculation testable by replacing a live exchange rate or clock with a fixed value.
  • Assert that a failed payment is never retried more than once.

More examples

The same check with Moq

Mock<T> wraps the double; .Object is the IEmailSender you pass to the code under test. Verify turns "was this called" into an assertion, which is the thing a hand-written recorder makes you write yourself. Reach for this when the interface has several members you do not care about, since Moq supplies them all for free. NSubstitute is the same idea with lighter syntax: Substitute.For<IEmailSender>() then sender.Received(1).Send("[email protected]", "Welcome").

Example · csharp
// dotnet add MyApp.Tests package Moq
using Moq;
using Xunit;

namespace Signup.Tests;

public class SignupServiceTests
{
    [Fact]
    public void Register_SendsOneWelcomeEmail()
    {
        // Generates a class implementing IEmailSender at run time.
        Mock<IEmailSender> email = new Mock<IEmailSender>();
        SignupService service = new SignupService(email.Object);

        service.Register("[email protected]");

        // Fails the test if that exact call did not happen exactly once.
        email.Verify(e => e.Send("[email protected]", "Welcome"), Times.Once());
    }
}

A stub for a value you need to control

Use a stub when the dependency answers a question and you only need the answer to be predictable. A live exchange rate would make the expected value unknowable, which is the whole problem a double solves. Note the stub is not verified: nothing checks that GetRate was called, because the point is the result, not the interaction.

Example · csharp
using Xunit;

namespace Pricing.Tests;

public interface IRateSource
{
    decimal GetRate(string currency);
}

// A stub: fixed answers, no recording, no logic worth testing.
public class StubRateSource : IRateSource
{
    public decimal GetRate(string currency) => currency == "EUR" ? 0.90m : 1.00m;
}

public class Converter
{
    private readonly IRateSource _rates;

    public Converter(IRateSource rates)
    {
        _rates = rates;
    }

    public decimal ToLocal(decimal amount, string currency) => amount * _rates.GetRate(currency);
}

public class ConverterTests
{
    [Fact]
    public void ToLocal_EuroAmount_UsesEuroRate()
    {
        Converter converter = new Converter(new StubRateSource());

        Assert.Equal(90m, converter.ToLocal(100m, "EUR"));
    }
}

A fake with real behaviour

A fake is worth writing when several tests need the dependency to actually remember things; a mocking library would need a Setup per call and the test would read as configuration rather than behaviour. The risk is that the fake drifts from the real implementation, so keep it simple and let integration tests cover the real database. Both => bodies here are shorthand: public void Add(string email) => _emails.Add(email); is the same as a body of { _emails.Add(email); }.

Example · csharp
using System.Collections.Generic;
using Xunit;

namespace Users.Tests;

public interface IUserStore
{
    void Add(string email);
    bool Exists(string email);
}

// A fake: a real, working implementation backed by a hash set.
public class InMemoryUserStore : IUserStore
{
    private readonly HashSet<string> _emails = new();

    public void Add(string email) => _emails.Add(email);

    public bool Exists(string email) => _emails.Contains(email);
}

public class UserStoreTests
{
    [Fact]
    public void Add_ThenExists_ReturnsTrue()
    {
        IUserStore store = new InMemoryUserStore();

        store.Add("[email protected]");

        Assert.True(store.Exists("[email protected]"));
    }
}

Discussion

  • Be the first to comment on this lesson.