.NET Unit Testing Best Practices with xUnit and Moq

Elevate your testing game by learning best practices for writing clean, maintainable, and effective unit tests in .NET using popular frameworks like xUnit and Moq.

Unit testing is a cornerstone of professional software development. It provides a safety net for refactoring, serves as living documentation, and ultimately leads to higher-quality code. In the .NET ecosystem, xUnit has become the de facto standard for a testing framework, while Moq is the go-to library for creating mock objects.

Let's go beyond the basics and explore some best practices for writing effective unit tests.

1. Follow the Arrange-Act-Assert (AAA) Pattern

This is the most fundamental best practice. Every test should have three distinct, clear sections:

  • Arrange: Set up the world for your test. This is where you create your objects, set up your mocks, and define any data needed for the test.
  • Act: Execute the single method or action that you are testing.
  • Assert: Verify the outcome. Check that the method behaved as expected, that return values are correct, or that mocks were called appropriately.
[Fact]
public void Add_ShouldReturnCorrectSum_WhenGivenTwoIntegers()
{
    // Arrange
    var calculator = new Calculator();
    int a = 2;
    int b = 3;

    // Act
    int result = calculator.Add(a, b);

    // Assert
    Assert.Equal(5, result);
}

This structure makes your tests incredibly easy to read and understand.

2. One Assert Per Test (Mostly)

A unit test should ideally test one single, logical concept. This often translates to having a single Assert statement. This makes it easier to diagnose failures—if a test fails, you know exactly which assertion failed without having to read through a list of them.

However, this is a guideline, not a strict rule. It's perfectly acceptable to have multiple assertions if they are all testing different facets of the same logical outcome. For example, when testing an object mapping, you might assert that several different properties were mapped correctly.

3. Use Mocks to Isolate Your System Under Test (SUT)

A unit test should test a single unit of code in isolation. This means you need to replace its external dependencies (like database repositories, API clients, or file systems) with mocks. Moq is a powerful library for creating these.

Consider a service that depends on a repository:

public interface IUserRepository
{
    User GetById(int userId);
}

public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public string GetUserGreeting(int userId)
    {
        var user = _userRepository.GetById(userId);
        return user != null ? $"Hello, {user.Name}!" : "User not found.";
    }
}

To test UserService, you don't want to hit a real database. So, you mock IUserRepository:

[Fact]
public void GetUserGreeting_ShouldReturnGreeting_WhenUserExists()
{
    // Arrange
    var mockUserRepository = new Mock<IUserRepository>();
    var expectedUser = new User { Id = 1, Name = "Alice" };

    // Set up the mock: when GetById(1) is called, return the expected user.
    mockUserRepository.Setup(repo => repo.GetById(1)).Returns(expectedUser);

    var userService = new UserService(mockUserRepository.Object);

    // Act
    string greeting = userService.GetUserGreeting(1);

    // Assert
    Assert.Equal("Hello, Alice!", greeting);
}

4. Name Your Tests Descriptively

Your test names should be clear and descriptive. A common and effective naming convention is MethodName_Should_ExpectedBehavior_When_State.

  • Good: GetUserGreeting_ShouldReturnGreeting_WhenUserExists
  • Bad: Test1 or GreetingTest

A good test name tells you exactly what the test is doing without you having to read a single line of its code.

5. Use [Theory] for Parameterized Tests

If you have multiple test cases that follow the same logic but with different data, don't write separate tests. Use xUnit's [Theory] attribute with [InlineData] to create a single, data-driven test.

[Theory]
[InlineData(2, 3, 5)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
public void Add_ShouldReturnCorrectSum_ForVariousInputs(int a, int b, int expected)
{
    // Arrange
    var calculator = new Calculator();

    // Act
    int result = calculator.Add(a, b);

    // Assert
    Assert.Equal(expected, result);
}

This keeps your test suite DRY (Don't Repeat Yourself) and makes it easy to add new test cases.

Conclusion

Writing good unit tests is a skill that takes practice. By following these best practices—structuring your tests with AAA, using mocks to isolate your code, naming your tests descriptively, and using theories for data-driven tests—you can create a robust test suite that improves your code quality and gives you the confidence to refactor and ship features faster.