Getting Started with FakeItEasy for Mocking and Unit Testing in .NET

When working with unit tests in C# and .NET, creating mock objects and simulating behaviors of dependencies are crucial steps. Enter FakeItEasy, a simple yet powerful mocking framework that can save you time and reduce boilerplate code.

In this blog, we’ll dive into FakeItEasy and demonstrate its usage in a practical scenario involving C# 11 and .NET 7. You’ll learn how to mock interfaces, simulate methods, and verify method calls to streamline your testing process.

Why FakeItEasy?

FakeItEasy stands out for its simplicity and ease of use. One of its best features is the automatic creation of fake objects with non-null values. For example:

  • Objects are automatically instantiated.
  • Errors return zero-length arrays, preventing null reference exceptions in many cases.

Setting Up the Demo

Here’s the setup for the demo:

  1. Classes and Interfaces:
    • User: A simple record type.
    • IUserProvider: Interface with methods like GetUsers (returns an array of User), Get (fetches a user by name), and GetName (returns the name asynchronously).
    • RewardCalculator: Contains a Calculate method to compute rewards for users based on a dictionary.
    • UserProcessor: Combines IUserProvider and RewardCalculator to process users and compute rewards.
  2. Objective: Test the UserProcessor class’s Process method to ensure its correctness.
public record User(string Name, string Address, int Age);

public interface IUserProvider
{
    User[] GetUsers();
    Use Get(string name);
    Task<string> GetName(User user);
}

public class UserProvider : IUserProvider
{
    public User[] GetUsers()
    {
        return default;
    }

    public Use Get(string name)
    {
        return default;
    }

    public async Task<string> GetName(User user)
    {
        return await Task.FromResult(user.Name);
    }

}

public class RewardCalculator
{
    private IDictionary<string, int> rewards = new Dictionary<string, int>
    {
        {"sam", 10},
        {"bob", 20}
    }

    public int Calculate(User user)
    {
        if(user==null) throw new ArgumentNullException("Null user");
        if(!rewards.ContainsKey(user.Name) throw new ArgumentException("User missing");

        return rewards[user.Name];
    }
}

public class UserProcessor
{
    private readonly IUserProvider userProvider;
    private readonly RewardCalculator rewardCalculator;

    public UserProcessor(IUserProvider userProvider, RewardCalculator rewardCalculator)
    {
        this.userProvider = userProvider;
        this.rewardCalculator = rewardCalculator;
    }

    public (bool, string) Process()
    {
        var users = userProvider.GetUsers();
        if(users == null || users.Length == 0) return (false, "No users");
        var reward = 0;
        foreach (var user in users)
        {
            reward += rewardCalculator.Calculate(user);
        } 

        return (true, reward.ToString());
    }

}

Using FakeItEasy

1. Creating Fakes

FakeItEasy allows creating fake objects for both interfaces and classes:

var provider = A.Fake<IUserProvider>();
var calculator = A.Fake<RewardCalculator>();

Note: You can only fake methods that are virtual or belong to an interface. Non-virtual methods will throw exceptions.

2. Setting Up Behavior

Fake behaviors can be defined for methods using A.CallTo:

A.CallTo(() => provider.GetUsers()).Returns(new[] { new User { Name = "Bob" } });

This setup returns a predefined user array when GetUsers is called.

3. Verifying Method Calls

You can verify if methods are called with specific parameters:

A.CallTo(() => provider.GetUsers()).MustHaveHappenedOnceExactly();

You can also ensure certain methods are not called:

A.CallTo(() => provider.Get("Test")).MustNotHaveHappened();

Advanced Features of FakeItEasy

Ordering Method Calls

FakeItEasy supports verifying the order of method calls:

A.CallTo(() => provider.GetUsers())
    .MustHaveHappenedOnceExactly()
    .Then(A.CallTo(() => provider.Get(A<string>.Ignored)).MustHaveHappenedOnceExactly());

Simulating Exceptions

You can simulate exceptions for specific methods:

A.CallTo(() => provider.GetUsers()).Throws<ArgumentException>();

Creating Fake Collections

FakeItEasy can generate collections of fake objects:

var fakeUsers = A.CollectionOfFake<User>(10);

Faking Delegates

We can even fake lambdas and delegates:

var fakeFunc = A.Fake<Func<string, int>>();

Common Pitfalls and Best Practices

  1. Non-Virtual Methods: Ensure methods you want to fake are virtual or part of an interface.
  2. Class-Level Fakes: Be cautious when reusing fakes across tests. Reset them or use test-level initialization to avoid conflicts.
  3. Exception Handling: Use frameworks like NUnit’s Assert.Catch to verify exception scenarios.

Sample Test Case

Here’s a simple test to validate the Process method:

[Test]
public void Should_ProcessUsersSuccessfully()
{
    // Arrange
    var provider = A.Fake<IUserProvider>();
    var calculator = A.Fake<RewardCalculator>();

    A.CallTo(() => provider.GetUsers()).Returns(new[] { new User { Name = "Bob" } });

    var processor = new UserProcessor(provider, calculator);

    // Act
    var (result, response) = processor.Process();

    // Assert
    Assert.IsTrue(result);
    Assert.IsNotNull(response);

    A.CallTo(() => provider.GetUsers()).MustHaveHappenedOnceExactly();
    A.CallTo(() => provider.Get(A<string>.Ignored)).MustNotHaveHappened();
}

Conclusion

FakeItEasy simplifies the testing process by allowing developers to quickly mock dependencies and verify method behaviors. Whether you’re dealing with simple interfaces or complex collections, FakeItEasy provides tools to ensure your unit tests are robust and maintainable.

If you’re looking to reduce boilerplate and focus on writing meaningful tests, give FakeItEasy a try. Happy coding!

The YouTube video for this coding experience is available here: https://youtu.be/1FgVcNPGvCY?si=JC8IF7noP9_lUPXE

If you found this guide helpful, please share it and consider subscribing for more C# and .NET insights!