Unit Testing in .NET Core - Mastering Mocking

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:

  1. 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.

  2. Control: Mocks allow you to control the behavior of dependencies, making it possible to test different scenarios and edge cases.

  3. 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:

  1. Use Interfaces: Define clear interfaces for your dependencies. This allows you to create mock implementations that adhere to these interfaces.

  2. 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:

  1. 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.

  2. 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:

  1. Dependency Injection: We injected the IUserRepository interface into the UserService, allows us to easily substitute the real repository that interacts with the database with a mock during testing.

  2. 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.

  1. In Visual Studio, right-click on your test project in the Solution Explorer and select "Manage NuGet Packages."

  2. 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 using Substitute.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 its UserExists method is called with the username argument using Returns(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 the userService with a unique username, "newuser".
  • In the Assert section:

    • It asserts that the result returned by RegisterUser is true because the username is unique.

    • userRepository.Received(1).AddUser(Arg.Any<User>()); verifies that the AddUser method of the mock userRepository was called exactly once with any User object as an argument. This checks that the RegisterUser method attempted to add a user.

    • userRepository.Received(1).SaveChanges(); checks that the SaveChanges method of the mock userRepository was called exactly once. This verifies that the RegisterUser 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 using Substitute.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 its UserExists method is called with the username argument using Returns(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 the userService with a duplicate username, "existinguser".
  • In the Assert section:

    • It asserts that the result returned by RegisterUser is false because the username already exists.

    • userRepository.DidNotReceive().AddUser(Arg.Any<User>()); verifies that the AddUser method of the mock userRepository was not called. This is because, in this scenario, the RegisterUser method should not attempt to add the user since the user already exists.

    • userRepository.DidNotReceive().SaveChanges(); checks that the SaveChanges method of the mock userRepository was not called. Again, in this scenario, the RegisterUser 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.

  1. In Visual Studio, right-click on your test project in the Solution Explorer and select "Manage NuGet Packages."

  2. 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/FeatureNSubstituteFakeItEasy
Creating a FakeSubstitute.For<T>()A.Fake<T>()
Configuring BehaviorSubstitute.For<T>().SomeMethod().Returns(value)A.CallTo(() => fake.SomeMethod()).Returns(value)
Argument MatchersArg.Is<T>(predicate)A<T>.That.Matches(predicate)
Verifying CallsReceived() and DidNotReceive()MustHaveHappened() and MustNotHaveHappened()
Argument CaptureAchieved using Arg.DoA.CallTo(() => fake.SomeMethod()).Invokes((args) => { /* capture args */ })
Property Setter Behaviorsub.SomeProperty = valueA.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