Unit Testing Scenarios with C#, xUnit, and Moq

Unit testing is an essential part of modern software development, enabling developers to ensure that individual components of their code work correctly in isolation. In the .NET ecosystem, C# is commonly used for building applications, and xUnit is a popular testing framework for writing and executing unit tests. When it comes to isolating units of work and ensuring that they function correctly, Moq is a powerful and flexible library that allows developers to create mock objects and set up expectations on them.

In this detailed article, we will explore various unit testing scenarios using C#, xUnit, and Moq, including how to test arguments passed to functions, track how many times a function is called, handle exceptional scenarios, and more. By the end of this article, you’ll have a comprehensive understanding of how to leverage these tools to write effective unit tests for your C# code.

1. Introduction to xUnit and Moq

xUnit is a testing framework for the .NET ecosystem that is simple and extensible. It provides a rich set of features for writing and running unit tests, including assertions, test case theory, and test fixtures.

Moq is a library for creating mock objects in .NET, allowing you to simulate the behavior of dependencies in your tests. It helps you isolate the unit of work by replacing real implementations with mock objects, which you can then configure and assert against.

2. Setting Up xUnit and Moq

Before diving into unit testing scenarios, ensure you have xUnit and Moq installed in your project. You can install these libraries via NuGet:

dotnet add package xunit
dotnet add package Moq

For writing xUnit tests, you also need the xunit.runner.visualstudio package if you are using Visual Studio:

dotnet add package xunit.runner.visualstudio

3. Basic Testing with xUnit

Let’s start with a simple example to illustrate the basics of xUnit. Assume we have a class Calculator with a method Add that we want to test.

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

Here is a simple xUnit test for the Add method:

using Xunit;

public class CalculatorTests
{
    [Fact]
    public void Add_ShouldReturnSumOfTwoNumbers()
    {
        // Arrange
        var calculator = new Calculator();

        // Act
        var result = calculator.Add(2, 3);

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

In this test, we use the [Fact] attribute to indicate that this is a test method. The test checks if the Add method returns the correct sum of two numbers.

4. Testing Function Arguments with Moq

Moq excels in scenarios where you need to verify interactions with mock objects. Let’s consider a service that uses a repository interface to fetch data:

public interface IRepository
{
    User GetUserById(int id);
}

public class UserService
{
    private readonly IRepository _repository;

    public UserService(IRepository repository)
    {
        _repository = repository;
    }

    public User GetUser(int id)
    {
        return _repository.GetUserById(id);
    }
}

We want to test the GetUser method and verify that GetUserById is called with the correct argument.

using Moq;
using Xunit;

public class UserServiceTests
{
    [Fact]
    public void GetUser_ShouldCallRepositoryWithCorrectId()
    {
        // Arrange
        var mockRepository = new Mock<IRepository>();
        var userService = new UserService(mockRepository.Object);

        // Act
        userService.GetUser(5);

        // Assert
        mockRepository.Verify(repo => repo.GetUserById(5), Times.Once);
    }
}

In this test, we use Moq to create a mock IRepository. We then set up the UserService and call GetUser. Using Verify, we assert that GetUserById was called with the argument 5 exactly once.

5. Verifying Function Call Counts

Continuing from the previous example, suppose we want to verify how many times a method is called. Moq provides the Times class for such assertions.

[Fact]
public void GetUser_ShouldCallRepositoryOnce()
{
    // Arrange
    var mockRepository = new Mock<IRepository>();
    var userService = new UserService(mockRepository.Object);

    // Act
    userService.GetUser(1);
    userService.GetUser(2);

    // Assert
    mockRepository.Verify(repo => repo.GetUserById(It.IsAny<int>()), Times.Exactly(2));
}

Here, Verify ensures that GetUserById was called exactly twice, regardless of the arguments passed.

6. Handling Exceptional Scenarios

Unit tests should also cover exceptional or edge cases. For example, suppose the GetUserById method should throw an exception if the user is not found:

public class UserService
{
    private readonly IRepository _repository;

    public UserService(IRepository repository)
    {
        _repository = repository;
    }

    public User GetUser(int id)
    {
        var user = _repository.GetUserById(id);
        if (user == null)
        {
            throw new UserNotFoundException($"User with id {id} not found.");
        }
        return user;
    }
}

We need to test this exceptional scenario:

public class UserServiceTests
{
    [Fact]
    public void GetUser_ShouldThrowExceptionIfUserNotFound()
    {
        // Arrange
        var mockRepository = new Mock<IRepository>();
        mockRepository.Setup(repo => repo.GetUserById(It.IsAny<int>())).Returns((User)null);
        var userService = new UserService(mockRepository.Object);

        // Act & Assert
        var exception = Assert.Throws<UserNotFoundException>(() => userService.GetUser(1));
        Assert.Equal("User with id 1 not found.", exception.Message);
    }
}

In this test, we use Setup to configure the mock repository to return null, which simulates a user not being found. Assert.Throws is used to verify that the exception is thrown with the expected message.

7. Mocking Properties and Methods

Moq also allows you to mock properties and methods. Suppose our repository interface includes a Count property:

public interface IRepository
{
    int Count { get; }
    User GetUserById(int id);
}

We can mock this property as follows:

[Fact]
public void Count_ShouldReturnExpectedValue()
{
    // Arrange
    var mockRepository = new Mock<IRepository>();
    mockRepository.Setup(repo => repo.Count).Returns(10);
    var userService = new UserService(mockRepository.Object);

    // Act
    var count = mockRepository.Object.Count;

    // Assert
    Assert.Equal(10, count);
}

Here, we use Setup to specify that Count should return 10. We then assert that the property returns the expected value.

8. Testing Asynchronous Methods

With the increasing use of asynchronous programming, it’s important to test asynchronous methods. Suppose we have an asynchronous method in our service:

public class UserService
{
    private readonly IRepository _repository;

    public UserService(IRepository repository)
    {
        _repository = repository;
    }

    public async Task<User> GetUserAsync(int id)
    {
        return await _repository.GetUserByIdAsync(id);
    }
}

We can test this asynchronous method using xUnit:

[Fact]
public async Task GetUserAsync_ShouldReturnUser()
{
    // Arrange
    var mockRepository = new Mock<IRepository>();
    var expectedUser = new User { Id = 1 };
    mockRepository.Setup(repo => repo.GetUserByIdAsync(1)).ReturnsAsync(expectedUser);
    var userService = new UserService(mockRepository.Object);

    // Act
    var user = await userService.GetUserAsync(1);

    // Assert
    Assert.Equal(expectedUser, user);
}

In this test, we use ReturnsAsync to set up the mock repository to return a User object asynchronously. The test then verifies that the returned user matches the expected user.

9. Using It.Is for Argument Matching

Moq allows you to match arguments using various It.Is methods. Suppose you want to verify that a method was called with a string argument that contains a certain substring:

[Fact]
public void SendMessage_ShouldCallSendWithMessageContainingSubstring()
{
    // Arrange
    var mockMessagingService = new Mock<IMessagingService>();
    var notificationService = new NotificationService(mockMessagingService.Object);

    // Act
    notificationService.SendNotification("Hello, World!");

    // Assert
    mockMessagingService.Verify(
        service => service.Send(It.Is<string>(msg => msg.Contains("World"))), 
        Times.Once
    );
}

Here, It.Is is used to specify that the Send method should be called with a message containing the substring “World”.

10. Verifying Interactions with Multiple Mocks

When working with multiple mocks, you might need to verify interactions across them. Consider a scenario where a NotificationService depends on both an IMessagingService and an ILogger:

public

 interface ILogger
{
    void Log(string message);
}

public class NotificationService
{
    private readonly IMessagingService _messagingService;
    private readonly ILogger _logger;

    public NotificationService(IMessagingService messagingService, ILogger logger)
    {
        _messagingService = messagingService;
        _logger = logger;
    }

    public void NotifyAndLog(string message)
    {
        _messagingService.Send(message);
        _logger.Log($"Message sent: {message}");
    }
}

We can test the interactions with both mocks:

[Fact]
public void NotifyAndLog_ShouldCallMessagingServiceAndLogger()
{
    // Arrange
    var mockMessagingService = new Mock<IMessagingService>();
    var mockLogger = new Mock<ILogger>();
    var notificationService = new NotificationService(mockMessagingService.Object, mockLogger.Object);

    // Act
    notificationService.NotifyAndLog("Test Message");

    // Assert
    mockMessagingService.Verify(service => service.Send("Test Message"), Times.Once);
    mockLogger.Verify(logger => logger.Log("Message sent: Test Message"), Times.Once);
}

In this test, we verify that both Send and Log methods are called exactly once with the expected arguments.

11. Conclusion

Unit testing with xUnit and Moq allows you to write robust tests for your C# code, covering various scenarios such as verifying function arguments, tracking method call counts, handling exceptions, and more. By leveraging these tools, you can ensure that your code behaves as expected and is resilient to changes and edge cases.

In summary:

  • xUnit provides a framework for writing and running tests.
  • Moq offers powerful capabilities for creating mock objects and setting up expectations.
  • Argument Matching allows you to verify that methods are called with the correct arguments.
  • Function Call Counts help you assert how many times a method is invoked.
  • Exceptional Scenarios ensure that your code handles errors gracefully.
  • Asynchronous Testing supports the testing of asynchronous methods.
  • Advanced Mocking features enable complex scenarios and interactions.

By mastering these techniques, you’ll be well-equipped to write effective unit tests and maintain high-quality code. Happy testing!

Leave a Reply