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 though out 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 request 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();
        var source = Substitute.For();
        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 where 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();

// 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 where called and in a specific order. This makes it hard to change internal structure without refactoring test.