Building complex systems, we understand the daily struggles that come with writing effective tests. One of the most common pitfalls is choosing the right fake object for your test. Imagine you’re trying to test a method that interacts with an external service, and you have multiple options for simulating this service.
The friction comes from wanting to use what’s convenient rather than what’s best for your test. You might find yourself tempted to use a simple FakeUserRepository class because it’s quick and doesn’t require any additional dependencies. However, this approach can quickly lead to tests that are hard to maintain and understand.
In this blog post, we’ll explore the different types of test doubles: fakes, stubs, mocks, and spies. Each type has its own use case, and choosing the right one can significantly improve the quality of your tests. We’ll also see code samples in C# using NSubstitute for each type.
By understanding these concepts and practicing their correct usage, you can write more reliable and maintainable test suites, which will ultimately help you deliver higher-quality software faster.
Let’s look at each testing concept and learn when to use them effectively!
Test Doubles
Test doubles are objects that mimic the behavior of real objects but don’t execute any real work. They are used in place of complex or slow objects during testing. This is the umbrella term that applies to fakes, stubs, mocks, and spies. They’re like stunt doubles in movies - they look like the real deal but are used for a very specific purpose. For example, suppose you’re working in a system that needs to confirm a user is active. This code shows how to use a version of IUserRepository for testing whether a user’s active state is being returned.
using NSubstitute;
using Xunit;
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public bool IsUserActive(string userId)
{
return _userRepository.GetUser(userId)?.IsActive ?? false;
}
}
public interface IUserRepository
{
User GetUser(string userId);
}
[Fact]
public void TestIsUserActive_WhenUserIsActive_ReturnsTrue()
{
var mockUserRepository = Substitute.For<IUserRepository>();
mockUserRepository.GetUser("123").Returns(new User { IsActive = true });
var userService = new UserService(mockUserRepository);
bool result = userService.IsUserActive("123");
Assert.True(result);
}The test double in this case can be seen in this piece of code:
var mockUserRepository = Substitute.For<IUserRepository>();
mockUserRepository.GetUser("123").Returns(new User { IsActive = true });We still follow the contract of IUserRepository. We are setting a controlled call to a method with a specific input - GetUser("123"). We are also setting a controlled return value using Returns().
Fakes
Fakes are implementations of interfaces or classes that are designed to work in a specific test scenario. They may be very basic or minimal. These are the simplest of the test doubles.
When it comes to fakes, the key thing to remember is that fakes use specific values. There are no references to Any or generic values.
This is our fake user repository example, with specific values for our users and their properties:
using System.Collections.Generic;
public class FakeUserRepository : IUserRepository
{
private readonly List<User> _users = new List<User>
{
new User { UserId = "123", IsActive = true },
new User { UserId = "456", IsActive = false }
};
public User GetUser(string userId)
{
return _users.Find(user => user.UserId == userId);
}
}
[Fact]
public void TestIsUserActive_WhenUsingFakeRepository_ReturnsTrue()
{
var fakeUserRepository = new FakeUserRepository();
var userService = new UserService(fakeUserRepository);
bool result = userService.IsUserActive("123");
Assert.True(result);
}Stubs
Stubs provide pre-programmed responses to calls made during the test. They do not verify the interactions with the object. They are used when you need to control the behavior of a dependency.
In this example, the UserStatusChecker class depends on an IExternalService. During testing, we want to simulate the behavior of the external service without actually making a real call. We can use a stub for this purpose.
public interface IExternalService
{
bool CheckUserStatus(string userId);
}
public class UserStatusChecker
{
private readonly IExternalService _externalService;
public UserStatusChecker(IExternalService externalService)
{
_externalService = externalService;
}
public bool IsUserActive(string userId)
{
return _externalService.CheckUserStatus(userId);
}
}
[Fact]
public void TestIsUserActive_WhenUsingStub_ReturnsTrue()
{
var stubExternalService = Substitute.For<IExternalService>();
stubExternalService.CheckUserStatus("123").Returns(true);
var userStatusChecker = new UserStatusChecker(stubExternalService);
bool result = userStatusChecker.IsUserActive("123");
Assert.True(result);
}In the example above, we are only providing a response to the CheckUserStatus method without verifying any interactions. We are not asserting that this method was called at all. Therefore, this is a stub and not a mock.
Mocks
Mocks are objects that verify the interaction with them during the test. They record calls made to them and can be used to assert that certain methods were called with specific parameters.
In this example, we are testing that GetUser("123") is called as part of the userService.IsUserActive("123") routine. The userService is using a mock user repository. Notice that we are not setting up any calls to Returns. This test specifically focuses on the interaction and not on the result.
[Fact]
public void TestIsUserActive_WhenCallingGetUser_MethodCalled()
{
var mockUserRepository = Substitute.For<IUserRepository>();
var userService = new UserService(mockUserRepository);
userService.IsUserActive("123");
mockUserRepository.Received(1).GetUser("123");
}In this case, we are verifying that the CheckUserStatus method was indeed called with the string “123”. This is a typical use case for mocks.
Spies
Spies are similar to mocks but do not verify the interaction; instead, they record the calls and provide access to the recorded data.
Notice that we don’t have the Received check in the assertions. This takes the approach of Assert.Equal to ensure that the method is getting called a specific number of times using a spy.
[Fact]
public void TestIsUserActive_WhenRecordingCalls_ReturnsTrue()
{
var spyUserRepository = new SpyUserRepository();
var userService = new UserService(spyUserRepository);
bool result = userService.IsUserActive("123");
Assert.True(result);
Assert.Equal(1, spyUserRepository.CallCount);
}
public class SpyUserRepository : IUserRepository
{
public int CallCount { get; private set; }
public User GetUser(string userId)
{
CallCount++;
return new User { UserId = "123", IsActive = true };
}
}In this example, we are recording calls to CheckUserStatus and verifying that it was indeed called with the string “123”.
Summary
For a quick reference of when to use these different test doubles:
- Fakes: Use when a simple implementation is sufficient for the test scenario, and you don’t want to use external frameworks.
- Stubs: Use when you need to control the behavior of a dependency in a specific way during the test.
- Mocks: Use when you need to verify that certain methods were called with specific parameters or to assert interaction patterns.
- Spies: Use when you need to record calls and provide access to the recorded data.
When used correctly, these test doubles can significantly improve the quality of your tests and make your code more reliable and maintainable. By understanding their appropriate use cases, you can avoid common pitfalls and write better tests that help ensure the reliability of your software.


