Skip to main content

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

bash
dotnet add package Moq

Basic Example

Let's start with a simple example. Imagine we have a service that sends emails:

csharp
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:

csharp
[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:

  1. NotificationService.NotifyUser returns the result from the email service
  2. The correct email address is constructed from the user ID
  3. 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:

csharp
// 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:

csharp
// 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:

csharp
// 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:

csharp
// 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:

csharp
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:

  1. Checks if a username is available
  2. Creates the user if the username is available
  3. Sends a welcome email

Here's how we might implement and test it:

csharp
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:

csharp
[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:

  1. Setting up different mock behaviors for different test cases
  2. Using mock verification to ensure the system interacts correctly with dependencies
  3. 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:

  1. Wrapper interfaces around sealed classes
  2. Dependency injection for static methods
  3. 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:

csharp
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:

csharp
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

Exercises

  1. Create a simple calculator service with a dependency on a logging interface. Write tests using mocks to verify that appropriate logging calls are made.

  2. Extend the user registration example to include email verification. Add tests that verify the verification process works correctly.

  3. Practice refactoring code to make it more testable by introducing interfaces for dependencies and using mock objects in tests.

  4. Experiment with different parameter matching techniques in Moq to understand how to verify complex method calls.

  5. 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! :)