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:
- Classes and Interfaces:
- User: A simple record type.
- IUserProvider: Interface with methods like
GetUsers
(returns an array ofUser
),Get
(fetches a user by name), andGetName
(returns the name asynchronously). - RewardCalculator: Contains a
Calculate
method to compute rewards for users based on a dictionary. - UserProcessor: Combines
IUserProvider
andRewardCalculator
to process users and compute rewards.
- Objective: Test the
UserProcessor
class’sProcess
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
- Non-Virtual Methods: Ensure methods you want to fake are virtual or part of an interface.
- Class-Level Fakes: Be cautious when reusing fakes across tests. Reset them or use test-level initialization to avoid conflicts.
- 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!