Unit Testing in .NET Core - Mastering Mocking
This is the fifth post in our Unit Testing in .NET Core series! In the previous post, we looked at writing better assertions in our unit tests. In this post, we will explore the concept of mocking in unit testing using xUnit, discuss different types of mocks, and show how to write testable code that supports mocking.
What is Mocking and Why Is It Used in Unit Testing?
Mocking is the process of creating simulated objects that mimic the behavior of real objects in a controlled way. In unit testing, mocking is used to isolate the unit being tested from its external dependencies, such as databases, web services, or other components. By doing so, you can ensure that your tests focus on the unit's logic and behavior, rather than the behavior of its dependencies.
Mocking serves several purposes:
Isolation: It isolates the unit under test from external dependencies, ensuring that any failures in the test are due to issues in the unit itself.
Control: Mocks allow you to control the behavior of dependencies, making it possible to test different scenarios and edge cases.
Performance: Mocking can improve test performance by replacing slow or resource-intensive dependencies with lightweight, in-memory mocks.
Mocks, Fakes, and Stubs
Before diving deeper into mocking, let's clarify some related terms:
Mocks: Mocks are objects that mimic the behavior of real objects but do not provide real implementations. They are used to verify interactions between the unit under test and its dependencies.
Fakes: Fakes are simplified implementations of dependencies that provide some basic functionality. They are often used when the real dependency is too complex or time-consuming to set up for testing.
Stubs: Stubs are similar to mocks but focus on providing predetermined responses to method calls, rather than verifying interactions. Stubs are used to control the flow of a test scenario.
Writing Testable Code That Supports Mocking
To write testable code that supports mocking, consider the following best practices:
Use Interfaces: Define clear interfaces for your dependencies. This allows you to create mock implementations that adhere to these interfaces.
Dependency Injection (DI): Implement dependency injection to provide dependencies to your classes through constructor injection or property injection. This makes it easy to substitute real dependencies with mocks during testing.
Let's take a practical example to illustrate the transformation of non-testable code into a testable version. We'll start with a simplified example of a user registration system and progressively refactor it to make it testable.
Non-Testable Code:
Suppose we have a UserService
class responsible for registering users. It interacts directly with a database to check for existing usernames and save new users. Here's a non-testable version of the code:
public class UserService
{
private readonly Database _db;
public UserService()
{
_db = new Database(); // Creating a direct dependency on the database.
}
public bool RegisterUser(string username)
{
if (_db.Users.Any(u => u.Username == username))
{
return false; // Username already taken.
}
_db.Users.Add(new User { Username = username });
_db.SaveChanges();
return true;
}
}
Why it's Non-Testable:
Direct Dependency on Database: The code directly creates an instance of
Database
, which makes it impossible to replace with a mock database during testing. This tightly couples the code to a real database, making it difficult to isolate the unit under test.Lack of Abstraction: There are no interfaces or abstractions for the database interactions. We cannot easily substitute the database with a mock or fake implementation for testing purposes.
Making it Testable:
To make the code testable, we'll apply the principles discussed earlier, including using interfaces for dependencies and implementing dependency injection(DI).
Step 1: Introduce an Interface for the Database interaction
We start by defining an interface, IUserRepository
, which abstracts the database interactions that deal with User entity:
public interface IUserRepository
{
bool UserExists(string username);
void AddUser(User user);
void SaveChanges();
}
Step 2: Refactor the UserService
for DI
We modify the UserService
class to accept an IUserRepository
interface through its constructor, promoting dependency injection:
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public bool RegisterUser(string username)
{
if (_userRepository.UserExists(username))
{
return false; // Username already taken.
}
var user = new User { Username = username };
_userRepository.AddUser(user);
_userRepository.SaveChanges();
return true;
}
}
Why it's Now Testable:
Dependency Injection: We injected the
IUserRepository
interface into theUserService
, allows us to easily substitute the real repository that interacts with the database with a mock during testing.Abstraction: We introduced an interface, which abstracts the database interactions and makes it possible to create a mock implementation.
With these changes, we've transformed non-testable code that tightly depended on a real database into testable code that can be easily tested with controlled, mock database interactions. Now let's write the unit tests for this method by supplying mocks for the database. Let's explore two libraries that will help us in mocking - NSubstitute and FakeItEasy.
NSubstitute
NSubstitute is a popular mocking framework for the .NET ecosystem, specifically designed to simplify the process of creating mock objects in unit tests.
Before using NSubstitute you will have to add the NSubstitute library from NuGet.
In Visual Studio, right-click on your test project in the Solution Explorer and select "Manage NuGet Packages."
Search for "NSubstitute" in the NuGet Package Manager and click "Install" to add it to your project.
If you are using Visual Studio Code you can make use of the .NET CLI to install the package. Open a terminal or command prompt and navigate to your project's directory. Then, run the following command:
dotnet add package NSubstitute
Now that we have NSubstitute set up, let's write a unit test for the RegisterUser
method of the UserService
class.
using Xunit;
using NSubstitute;
public class UserServiceTests
{
[Fact]
public void RegisterUser_WhenUserDoesNotExist_ShouldReturnTrue()
{
// Arrange
string username = "newuser";
var userRepository = Substitute.For<IUserRepository>(); // creates mock repository
// Simulate that the username is not already in the database.
userRepository.UserExists(username).Returns(false);
var userService = new UserService(userRepository);
// Act
bool result = userService.RegisterUser(username);
// Assert
Assert.True(result);
userRepository.Received(1).AddUser(Arg.Any<User>());
userRepository.Received(1).SaveChanges();
}
[Fact]
public void RegisterUser_WhenUserExists_ShouldReturnFalse()
{
// Arrange
string username = "existinguser";
var userRepository = Substitute.For<IUserRepository>();
// Simulate that the username is already exists in the database.
userRepository.UserExists(username).Returns(true);
var userService = new UserService(userRepository);
// Act
bool result = userService.RegisterUser(username);
// Assert
Assert.False(result);
userRepository.DidNotReceive().AddUser(Arg.Any<User>());
userRepository.DidNotReceive().SaveChanges();
}
}
Let's examine the first test method RegisterUser_UniqueUsername_ReturnsTrue
:
In the
Arrange
section:It creates a mock (substitute) of the
IUserRepository
interface usingSubstitute.For<IUserRepository>()
. This mock will simulate the behavior of the database interactions of the user repository.It creates an instance of the
UserService
class, passing the mock user repository as a dependency.We set up the mock to return
false
when itsUserExists
method is called with theusername
argument usingReturns(false)
. This is to simulate the scenario where the user does not exist in the repository.
In the
Act
section:- It calls the
RegisterUser
method of theuserService
with a unique username, "newuser".
- It calls the
In the
Assert
section:It asserts that the result returned by
RegisterUser
istrue
because the username is unique.userRepository.Received(1).AddUser(Arg.Any<User>());
verifies that theAddUser
method of the mockuserRepository
was called exactly once with anyUser
object as an argument. This checks that theRegisterUser
method attempted to add a user.userRepository.Received(1).SaveChanges();
checks that theSaveChanges
method of the mockuserRepository
was called exactly once. This verifies that theRegisterUser
method attempted to save changes to the repository.
Now let's examine the second test method RegisterUser_DuplicateUsername_ReturnsFalse
:
In the
Arrange
section:It creates a mock (substitute) of the
IUserRepository
interface usingSubstitute.For<IUserRepository>()
. This mock will simulate the behavior of the database interactions of the user repository.It creates an instance of the
UserService
class, passing the mock user repository as a dependency.We set up the mock to return
true
when itsUserExists
method is called with theusername
argument usingReturns(true)
. This is to simulate the scenario where the user already exists in the repository.
In the
Act
section:- It calls the
RegisterUser
method of theuserService
with a duplicate username, "existinguser".
- It calls the
In the
Assert
section:It asserts that the result returned by
RegisterUser
isfalse
because the username already exists.userRepository.DidNotReceive().AddUser(Arg.Any<User>());
verifies that theAddUser
method of the mockuserRepository
was not called. This is because, in this scenario, theRegisterUser
method should not attempt to add the user since the user already exists.userRepository.DidNotReceive().SaveChanges();
checks that theSaveChanges
method of the mockuserRepository
was not called. Again, in this scenario, theRegisterUser
method should not attempt to save changes since the registration is unsuccessful.
The tests verify that the RegisterUser
method of the UserService
class behaves correctly under both unique and duplicate username conditions.
FakeItEasy
FakeItEasy is another popular open-source mocking framework for .NET that is used primarily in unit testing to create fake or mock objects.
Before using FakeItEasy you will have to add the FakeItEasy library from Nuget.
In Visual Studio, right-click on your test project in the Solution Explorer and select "Manage NuGet Packages."
Search for "FakeItEasy" in the NuGet Package Manager and click "Install" to add it to your project.
If you are using Visual Studio Code you can make use of the .NET CLI to install the package. Open a terminal or command prompt and navigate to your project's directory. Then, run the following command:
dotnet add package FakeItEasy
Now that we have FakeItEasy set up, let's write a unit test for the RegisterUser
method of the UserService
class.
using Xunit;
using FakeItEasy;
public class UserServiceTests
{
[Fact]
public void RegisterUser_WhenUserDoesNotExist_ShouldReturnTrue()
{
// Arrange
string username = "newuser";
var userRepository = A.Fake<IUserRepository>(); // Create a fake repository
// Simulate that the username does not already exist in the repository.
A.CallTo(() => userRepository.UserExists(username)).Returns(false);
var userService = new UserService(userRepository);
// Act
bool result = userService.RegisterUser(username);
// Assert
Assert.True(result);
A.CallTo(() => userRepository.AddUser(A<User>.Ignored)).MustHaveHappenedOnceExactly();
A.CallTo(() => userRepository.SaveChanges()).MustHaveHappenedOnceExactly();
}
[Fact]
public void RegisterUser_WhenUserExists_ShouldReturnFalse()
{
// Arrange
string username = "existinguser";
var userRepository = A.Fake<IUserRepository>();
// Simulate that the username already exists in the repository.
A.CallTo(() => userRepository.UserExists(username)).Returns(true);
var userService = new UserService(userRepository);
// Act
bool result = userService.RegisterUser(username);
// Assert
Assert.False(result);
A.CallTo(() => userRepository.AddUser(A<User>.Ignored)).MustNotHaveHappened();
A.CallTo(() => userRepository.SaveChanges()).MustNotHaveHappened();
}
}
The code is similar to the code we used in the unit tests written using NSubstitute. In this code, we replaced Substitute.For
with A.Fake
to create fake objects for the IUserRepository
. We also changed the way assertions are set up using A.CallTo
and MustHaveHappenedOnceExactly
or MustNotHaveHappened
for verifying method calls. The syntax and concepts are similar to NSubstitute, but they follow the FakeItEasy conventions.
Comparison between NSubstitute and FakeItEasy
Here's a comparison table between NSubstitute and FakeItEasy, highlighting the different operations and syntax available in both mocking frameworks for common operations:
Operation/Feature | NSubstitute | FakeItEasy |
Creating a Fake | Substitute.For<T>() | A.Fake<T>() |
Configuring Behavior | Substitute.For<T>().SomeMethod().Returns(value) | A.CallTo(() => fake.SomeMethod()).Returns(value) |
Argument Matchers | Arg.Is <T>(predicate) | A<T>.That.Matches(predicate) |
Verifying Calls | Received() and DidNotReceive() | MustHaveHappened() and MustNotHaveHappened() |
Argument Capture | Achieved using Arg.Do | A.CallTo(() => fake.SomeMethod()).Invokes((args) => { /* capture args */ }) |
Property Setter Behavior | sub.SomeProperty = value | A.CallToSet(() => fake.SomeProperty).To(value) |
Please note that while the core concepts and functionalities are similar, there may be some differences in syntax and capabilities between NSubstitute and FakeItEasy.
Summary
Mocking is a valuable technique in unit testing that allows you to isolate and control dependencies, ensuring that your tests focus on the specific behavior of the unit under test. By following best practices like defining interfaces and using dependency injection, you can write testable code that supports mocking. Libraries like NSubstitute and FakeItEasy simplify the process of creating and working with mocks, making your unit tests more effective and reliable.
References
Subscribe to my newsletter
Read articles from Geo J Thachankary directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by