.NET Mock Objects
Introduction
When writing unit tests in .NET, you'll often encounter scenarios where the component you're testing depends on other components or external resources. These dependencies can make testing difficult because:
- External systems might be slow or unavailable during testing
- Setting up the right test data in dependencies can be complex
- You want to test your component in isolation from its dependencies
This is where mock objects come into play. Mock objects are simulated objects that mimic the behavior of real dependencies but are controlled by your test. They allow you to focus on testing just the code you care about without worrying about the behavior of dependencies.
In this tutorial, we'll explore how to use mock objects in .NET testing, focusing primarily on the popular mocking framework, Moq.
Understanding Mocks vs. Stubs
Before diving into code examples, it's important to understand the terminology:
- Stub: A simple object that returns pre-configured responses to method calls
- Mock: A more sophisticated object that records how it was called and can verify expectations
While the distinction isn't always critical, it helps to know that mocking frameworks typically support both behaviors.
Getting Started with Moq
Moq is one of the most popular mocking frameworks for .NET. Let's see how to set it up and use it.
Installation
You can add Moq to your test project using either the NuGet Package Manager or the .NET CLI:
dotnet add package Moq
Basic Example
Let's start with a simple example. Imagine we have a service that sends emails:
public interface IEmailService
{
bool SendEmail(string to, string subject, string body);
}
public class NotificationService
{
private readonly IEmailService _emailService;
public NotificationService(IEmailService emailService)
{
_emailService = emailService;
}
public bool NotifyUser(string userId, string message)
{
// In a real application, we might look up the user's email
string email = $"{userId}@example.com";
return _emailService.SendEmail(email, "Notification", message);
}
}
Now, to test the NotificationService
without sending actual emails, we can mock the IEmailService
:
[Fact]
public void NotifyUser_SendsEmailWithCorrectParameters()
{
// Arrange
var mockEmailService = new Mock<IEmailService>();
// Setup the mock to return true when SendEmail is called with any parameters
mockEmailService
.Setup(service => service.SendEmail(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(true);
var notificationService = new NotificationService(mockEmailService.Object);
// Act
bool result = notificationService.NotifyUser("user123", "Hello, world!");
// Assert
Assert.True(result);
// Verify that SendEmail was called with the expected parameters
mockEmailService.Verify(
service => service.SendEmail(
"[email protected]",
"Notification",
"Hello, world!"
),
Times.Once
);
}
This test verifies that:
NotificationService.NotifyUser
returns the result from the email service- The correct email address is constructed from the user ID
- The email service is called with the expected parameters
Setting Up Mock Behavior
Returning Specific Values
You can configure a mock to return specific values when methods are called:
// Return true for any call to SendEmail
mockEmailService
.Setup(service => service.SendEmail(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(true);
// Return true only for specific parameters
mockEmailService
.Setup(service => service.SendEmail("[email protected]", "Hello", "Test message"))
.Returns(true);
Throwing Exceptions
You can make your mock throw exceptions to test error handling:
// Throw an exception when SendEmail is called
mockEmailService
.Setup(service => service.SendEmail(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Throws(new InvalidOperationException("Email service unavailable"));
Conditional Responses
You can use callback functions to implement more complex logic:
// Return true only for emails to domains other than "example.com"
mockEmailService
.Setup(service => service.SendEmail(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns((string to, string subject, string body) =>
!to.EndsWith("@example.com"));
Verifying Interactions
One of the most powerful features of mocks is the ability to verify how they were used:
// Verify SendEmail was called exactly once with any parameters
mockEmailService.Verify(
service => service.SendEmail(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()),
Times.Once
);
// Verify SendEmail was called exactly once with specific parameters
mockEmailService.Verify(
service => service.SendEmail("[email protected]", "Notification", "Hello, world!"),
Times.Once
);
// Verify SendEmail was never called with a specific domain
mockEmailService.Verify(
service => service.SendEmail(It.Is<string>(email => email.EndsWith("@gmail.com")),
It.IsAny<string>(),
It.IsAny<string>()),
Times.Never
);
Mocking Properties
You can also mock properties in interfaces:
public interface IUserRepository
{
int UserCount { get; }
User GetUserById(string id);
}
// In your test
var mockRepository = new Mock<IUserRepository>();
mockRepository.Setup(repo => repo.UserCount).Returns(100);
Real-World Example: A User Registration Service
Let's look at a more complete example. Imagine we have a user registration service that:
- Checks if a username is available
- Creates the user if the username is available
- Sends a welcome email
Here's how we might implement and test it:
public interface IUserRepository
{
bool UsernameExists(string username);
void CreateUser(User user);
}
public class User
{
public string Username { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
}
public class RegistrationResult
{
public bool Success { get; set; }
public string ErrorMessage { get; set; }
}
public class UserRegistrationService
{
private readonly IUserRepository _userRepository;
private readonly IEmailService _emailService;
public UserRegistrationService(IUserRepository userRepository, IEmailService emailService)
{
_userRepository = userRepository;
_emailService = emailService;
}
public RegistrationResult RegisterUser(string username, string email, string password)
{
// Check if username already exists
if (_userRepository.UsernameExists(username))
{
return new RegistrationResult
{
Success = false,
ErrorMessage = "Username already exists"
};
}
// Create the user
var user = new User
{
Username = username,
Email = email,
PasswordHash = HashPassword(password)
};
_userRepository.CreateUser(user);
// Send welcome email
_emailService.SendEmail(
email,
"Welcome to our service",
$"Hi {username}, thank you for registering!"
);
return new RegistrationResult { Success = true };
}
private string HashPassword(string password)
{
// In a real implementation, this would properly hash the password
return Convert.ToBase64String(
System.Security.Cryptography.SHA256.Create()
.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password))
);
}
}
Now, let's write a comprehensive test for the registration service:
[Fact]
public void RegisterUser_WithNewUsername_RegistersUserAndSendsEmail()
{
// Arrange
var mockRepository = new Mock<IUserRepository>();
var mockEmailService = new Mock<IEmailService>();
// Setup repository to say username doesn't exist
mockRepository
.Setup(repo => repo.UsernameExists("newuser"))
.Returns(false);
var registrationService = new UserRegistrationService(
mockRepository.Object,
mockEmailService.Object
);
// Act
var result = registrationService.RegisterUser(
"newuser",
"[email protected]",
"password123"
);
// Assert
Assert.True(result.Success);
Assert.Null(result.ErrorMessage);
// Verify user was created
mockRepository.Verify(
repo => repo.CreateUser(It.Is<User>(u =>
u.Username == "newuser" &&
u.Email == "[email protected]")),
Times.Once
);
// Verify welcome email was sent
mockEmailService.Verify(
service => service.SendEmail(
"[email protected]",
"Welcome to our service",
It.Is<string>(body => body.Contains("newuser") && body.Contains("thank you"))
),
Times.Once
);
}
[Fact]
public void RegisterUser_WithExistingUsername_ReturnsErrorAndDoesNotCreateUser()
{
// Arrange
var mockRepository = new Mock<IUserRepository>();
var mockEmailService = new Mock<IEmailService>();
// Setup repository to say username already exists
mockRepository
.Setup(repo => repo.UsernameExists("existinguser"))
.Returns(true);
var registrationService = new UserRegistrationService(
mockRepository.Object,
mockEmailService.Object
);
// Act
var result = registrationService.RegisterUser(
"existinguser",
"[email protected]",
"password123"
);
// Assert
Assert.False(result.Success);
Assert.Equal("Username already exists", result.ErrorMessage);
// Verify user was NOT created
mockRepository.Verify(
repo => repo.CreateUser(It.IsAny<User>()),
Times.Never
);
// Verify NO email was sent
mockEmailService.Verify(
service => service.SendEmail(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>()
),
Times.Never
);
}
This example demonstrates:
- Setting up different mock behaviors for different test cases
- Using mock verification to ensure the system interacts correctly with dependencies
- Testing both successful and error scenarios
Best Practices for Using Mocks
1. Only Mock What You Need To
Mock objects are most useful at the boundaries of your system. Don't mock every object - only those that:
- Connect to external systems
- Have complex setup requirements
- Introduce randomness or unpredictable behavior
- Are slow to initialize or run
2. Prefer Mocking Interfaces Over Concrete Classes
Interfaces are designed for abstraction and are easier to mock. Consider designing your code with testing in mind by using interfaces for dependencies.
3. Don't Over-Verify
Verifying every interaction with a mock object can make tests brittle. Focus on verifying the interactions that matter for the behavior you're testing.
4. Keep Mocks Simple
If you find yourself setting up complex sequences of mock behaviors, it might indicate that your code under test is doing too much or has too many dependencies.
Common Pitfalls and Solutions
Mocking Sealed Classes or Static Methods
Moq can't directly mock sealed classes or static methods. For these cases, you might need:
- Wrapper interfaces around sealed classes
- Dependency injection for static methods
- A different mocking framework like TypeMock Isolator (commercial) or Microsoft Fakes
Complex Parameter Matching
When verifying calls with complex objects, use the It.Is<T>()
method with a predicate:
mockRepository.Verify(repo => repo.CreateUser(
It.Is<User>(u =>
u.Username == "testuser" &&
u.Email == "[email protected]" &&
u.PasswordHash != null
)
));
Mocking Async Methods
For async methods, Moq provides a ReturnsAsync
method:
public interface IAsyncRepository
{
Task<User> GetUserAsync(string id);
}
var mockRepository = new Mock<IAsyncRepository>();
mockRepository
.Setup(repo => repo.GetUserAsync("user1"))
.ReturnsAsync(new User { Username = "user1", Email = "[email protected]" });
Summary
Mock objects are a powerful tool for isolating components during testing. By using mocks, you can:
- Create faster, more reliable tests that don't depend on external systems
- Test error scenarios that would be difficult to produce with real dependencies
- Focus your tests on specific components without worrying about the behavior of their dependencies
In .NET, Moq provides a flexible, intuitive API for creating and using mock objects. By understanding the basics of setting up mock behaviors and verifying interactions, you can write more effective unit tests that help ensure your code works correctly.
Additional Resources
- Moq GitHub Repository
- Moq Quickstart Documentation
- Martin Fowler's article on Test Doubles
- Microsoft's documentation on unit testing in .NET
Exercises
-
Create a simple calculator service with a dependency on a logging interface. Write tests using mocks to verify that appropriate logging calls are made.
-
Extend the user registration example to include email verification. Add tests that verify the verification process works correctly.
-
Practice refactoring code to make it more testable by introducing interfaces for dependencies and using mock objects in tests.
-
Experiment with different parameter matching techniques in Moq to understand how to verify complex method calls.
-
Create a test that verifies exception handling behavior using a mock that throws exceptions under specific conditions.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)