Mastery of TDD

Breaking Dependencies

Learn how to break dependencies in TDD. Use test doubles, stubs, and mocks to improve your test-driven development workflow with TDD Buddy.

In Test-Driven Development (TDD), it’s a common practice to use simplified objects that mimic the behavior of production objects. These objects, known as Test Doubles, help to decouple the code from production dependencies, making it more testable and preventing side effects like unwanted database updates.

Often, the term “Mock” is used generically to refer to various types of test doubles. However, conflating different types of test doubles can influence test design and potentially increase test fragility, making future refactoring challenging.

XUnit patterns recommend five types of test doubles, but in this discussion, we’ll focus on the three most prevalent types: Fakes, Stubs, and Mocks.

Fake

“An object with a simplified working implementation”

An example of a fake object could be a user repository that does not connect to a DB but instead uses an in-memory list to store data, thus allowing us to test the fetch user use case without performing time-consuming DB activities.

Beyond making it easier to test, fake objects can be used to prototype functionality by deferring key decisions.

Fake Object Diagram

Figure 1: Fake Object Diagram

Here is the code for the diagram

public class FakeUserRepository : IUserRepository
{
  private readonly List<UserAccount> _accounts = new List<UserAccount>();

  public FakeUserRepository()
  {
      _accounts.Add(new UserAccount { Email = "jane@mail.com", UserType = "Admin"});
  }

  public UserAccount FetchUser(string email)
  {
      return _accounts.FirstOrDefault(account => account.Email == email);
  }

  public void AddUser(UserAccount user)
  {
      if (UserExist(user)) return;

      _accounts.Add(user);
  }

  private bool UserExist(UserAccount user)
  {
      return !_accounts.Contains(user);
  }
}

public interface IUserRepository
{
    UserAccount FetchUser(string email);
    void AddUser(UserAccount user);
}

Instead of calling through to the DB, the FakeUserRepository makes use of an in-memory list for the operations fetch and add. The list remains mutable throughout the lifetime of the test, this fact is unique to Fake Objects.

Stub

“An object that holds predefined data and uses it to respond to method calls during a test run”

An example of this is the Character Copy kata. In this kata a Copier object needs to read from a source and write to a destination without using production objects.

To achieve this, one would then Stub out the source, as per the interface, to respond with specific data when called. This concept differs from Fake Object in that no state is saved to be used later in the test execution. Stubs are simple in that they merely respond to requests with predefined data.

Stub Object Diagram

Figure 2: Stub Object Diagram

Here is the code for the diagram

public class Copier
{
    private readonly ISource _source;
    private readonly IDestination _destination;

    public Copier(ISource source, IDestination destination)
    {
        _source = source;
        _destination = destination;
    }

    public void Copy()
    {
        var characterToRead = _source.ReadChar();
        while (characterToRead != '\n')
        {
            _destination.WriteChar(characterToRead);
            characterToRead = _source.ReadChar();
        }
    }
}

public interface IDestination
{
    void WriteChar(char c);
}

public interface ISource
{
    char ReadChar();
}

[TestFixture]
public class CopierTest
{
    [Test]
    public void Copy_WhenManyCharacters_ShouldCopyUntilNewline()
    {
        // Arrange
        var destination = Substitute.For<IDestination>();
        var source = Substitute.For<ISource>();
        source.ReadChar().Returns('z', 'x', 'y', '\n', 'a'); // ** Stubbing out the response data for ReadChar()
        var copier = new Copier(source, destination);
        // Act
        copier.Copy();
        // Assert
        source.Received(4).ReadChar();
    }
}

Instead of calling through to a file or keyboard, ISource is stubbed out to return predefined data when the ReadChar method is called. Unlike the Fake Object, its data is immutable during test execution. This results in a test object that is free from side effects and can be well molded to fit the specific scenario expressed in each test.

Mock

“An object that registers calls received. The test then verifies that the expected method calls were performed”

Mocks are used when we do not wish to invoke production code or when there is no easy way to verify the intended code was executed. An example is a password reset feature where we would never want to send an actual email, instead, we would want to verify that the email service was called.

Mock Object Diagram

Figure 3: Mock object diagram

Here is the code for the diagram

// Suppose we have the following interface for an email service
public interface IEmailService
{
    void SendEmail(string recipient, string subject, string body);
}

// We want to test a password reset feature that uses the email service
public class PasswordResetService
{
    private IEmailService _emailService;

    public PasswordResetService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public void ResetPassword(string email)
    {
        // Code to reset password goes here
        // ...

        // Send email notification
        _emailService.SendEmail(email, "Password Reset", "Your password has been reset.");
    }
}

// We can use Moq to create a mock of the email service
var mockEmailService = new Mock<IEmailService>();

// We expect the SendEmail method to be called once with specific arguments
mockEmailService.Setup(es => es.SendEmail("test@example.com", "Password Reset", "Your password has been reset."));

// Create an instance of the password reset service using the mock email service
var passwordResetService = new PasswordResetService(mockEmailService.Object);

// Call the ResetPassword method
passwordResetService.ResetPassword("test@example.com");

// Verify that the expected method was called exactly once
mockEmailService.Verify();

It is very easy to misuse this type of Test Double. One common mis-use is called the ‘Leaky Abstraction Trap’. An example would be a test that asserts methods were called and in a specific order. This makes it hard to change internal structure without refactoring tests.

Mocks as Behavioral Specifications

Used well, mocks do more than substitute for slow or unreachable dependencies. They specify collaboration behavior — the contract between an object and the services it depends on.

A state-based test answers “given these inputs, what result comes out?” A collaboration test answers “given this input, which services does the object talk to, and how?” Both are legitimate specifications, and some behaviors can only be expressed as the second.

Consider confirming an order:

[Test]
public void Confirming_an_order_reserves_the_items_with_the_warehouse()
{
    var warehouse = Substitute.For<IWarehouse>();
    var orders = new OrderService(warehouse, ...);

    orders.Confirm(anOrder().containing(twoBooks()));

    warehouse.Received().Reserve(Arg.Is<Reservation>(r =>
        r.Items.Count == 2 && r.Priority == Priority.Standard));
}

The test name reads as a behavioral claim: confirming an order reserves the items with the warehouse at standard priority. The mock verification is how that claim is enforced.

This is the discipline championed in Growing Object-Oriented Software, Guided by Tests (Freeman & Pryce, 2009), often called “London-school” or “mockist” TDD. The idea: design emerges from the conversations between objects, and those conversations are specified in tests through mocks.

When to Use Mocks This Way

  • The behavior is an interaction, not a return value. “Send the email”, “publish the event”, “reserve the stock.”
  • The collaboration is the design intent. The test is pinning down which object talks to which — exactly what you want to specify before the implementation exists.
  • The collaborator is an abstraction you own. Mocking your own interfaces pushes you toward small, role-based contracts. Mocking concrete classes or third-party libraries is a smell.

When Not To

  • The behavior is a pure calculation. State-based tests are clearer.
  • You find yourself asserting call order across three or more collaborators. That’s the Leaky Abstraction Trap — the test is coupled to implementation structure, not behavior.
  • You’re mocking to avoid setting up state. Use a Test Data Builder instead.

Pairing Mocks with Builders

Collaboration tests use builders for the input and mocks for the collaborators:

[Test]
public void Confirming_a_priority_order_reserves_items_with_priority_shipping()
{
    var warehouse = Substitute.For<IWarehouse>();
    var orders = new OrderService(warehouse, ...);
    var order = anOrder().forCustomer(aPriorityMember()).containing(twoBooks());

    orders.Confirm(order);

    warehouse.Received().Reserve(Arg.Is<Reservation>(r =>
        r.Priority == Priority.Expedited));
}

Builders carry the domain vocabulary. Mocks carry the collaboration contract. Together they produce tests that read as behavioral specifications and survive refactors of the internals.

Further Reading