Most C# developers think unit testing is about isolating code. It’s not. It’s about communicating intent.
Let’s see it in action. Imagine a UserService that depends on an IUserRepository to fetch user data.
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public string GetUserName(int userId)
{
var user = _userRepository.GetUserById(userId);
if (user == null)
{
return "User not found";
}
return user.Name;
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public interface IUserRepository
{
User GetUserById(int id);
void SaveUser(User user);
}
Here’s how you’d test GetUserName using Moq:
using Moq;
using Xunit;
public class UserServiceTests
{
[Fact]
public void GetUserName_UserExists_ReturnsUserName()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
var expectedUserName = "Alice";
var userId = 123;
mockRepo.Setup(repo => repo.GetUserById(userId))
.Returns(new User { Id = userId, Name = expectedUserName });
var userService = new UserService(mockRepo.Object);
// Act
var actualUserName = userService.GetUserName(userId);
// Assert
Assert.Equal(expectedUserName, actualUserName);
}
[Fact]
public void GetUserName_UserDoesNotExist_ReturnsNotFoundMessage()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
var userId = 456;
mockRepo.Setup(repo => repo.GetUserById(userId))
.Returns((User)null); // Explicitly return null
var userService = new UserService(mockRepo.Object);
// Act
var actualMessage = userService.GetUserName(userId);
// Assert
Assert.Equal("User not found", actualMessage);
}
}
And with NSubstitute:
using NSubstitute;
using Xunit;
public class UserServiceTestsNSubstitute
{
[Fact]
public void GetUserName_UserExists_ReturnsUserName()
{
// Arrange
var fakeRepo = Substitute.For<IUserRepository>();
var expectedUserName = "Bob";
var userId = 789;
fakeRepo.GetUserById(userId).Returns(new User { Id = userId, Name = expectedUserName });
var userService = new UserService(fakeRepo);
// Act
var actualUserName = userService.GetUserName(userId);
// Assert
Assert.Equal(expectedUserName, actualUserName);
}
[Fact]
public void GetUserName_UserDoesNotExist_ReturnsNotFoundMessage()
{
// Arrange
var fakeRepo = Substitute.For<IUserRepository>();
var userId = 101;
fakeRepo.GetUserById(userId).Returns((User)null);
var userService = new UserService(fakeRepo);
// Act
var actualMessage = userService.GetUserName(userId);
// Assert
Assert.Equal("User not found", actualMessage);
}
}
The problem these libraries solve is dependency. In real applications, your classes often rely on other classes or services. If you tried to test UserService by using a real IUserRepository implementation (e.g., one that talks to a database), your tests would be slow, fragile, and dependent on external state. Mocking frameworks like Moq and NSubstitute let you create "fake" versions of these dependencies. You tell these fakes exactly how to behave when specific methods are called, allowing you to isolate the code you’re actually testing.
The core idea is to replace the real dependency with a "mock" or "stub" object. This fake object implements the same interface or inherits from the same base class as the real dependency. You then configure the mock object to return specific values or perform specific actions when its methods are invoked with certain arguments. This is achieved through methods like Setup (Moq) or Returns (NSubstitute). The mock.Object (Moq) or the fake object itself (NSubstitute) is then passed to the class under test.
When you call userService.GetUserName(userId), the UserService calls _userRepository.GetUserById(userId). Because _userRepository is a mock, it doesn’t go to a database. Instead, it checks its configuration: "if GetUserById is called with userId = 123, return this specific User object." This allows you to control the input to your UserService logic precisely, simulating different scenarios (user exists, user doesn’t exist, repository throws an error, etc.) without needing to set up complex external environments.
The primary levers you control are the method calls on the dependency and the return values or exceptions those calls produce. You can also assert that methods on the mock were called, and with what arguments, verifying that your code under test interacted with its dependencies as expected. For example, to check if SaveUser was called:
Moq:
mockRepo.Verify(repo => repo.SaveUser(It.IsAny<User>()), Times.Once());
NSubstitute:
fakeRepo.Received(1).SaveUser(Arg.Any<User>());
These verification calls ensure your code does what it’s supposed to do with its collaborators, not just that it produces the right output given certain inputs.
A subtle but critical aspect of mocking is understanding the difference between stubs and mocks. Stubs are generally configured to return specific values (like Returns(new User { ... })). Mocks, on the other hand, are often used to verify behavior – did a method get called? Did it get called with the right parameters? While Moq and NSubstitute can both do both, this distinction helps in designing tests that clearly communicate what you’re testing: the outcome of a calculation (stubbing is good) or the interaction with another service (mocking/verification is good). If your test only verifies that a method was called and doesn’t assert anything about the return value or the state change, you might be testing the dependency more than your own code.
The next hurdle is learning how to mock methods that return void, handle exceptions, and deal with more complex argument matching.