.Net Core
Testing Asynch Code

How to do Unit Testing of Asynch Code?

Unit testing asynchronous code is essential to ensure that asynchronous methods in your application work correctly and reliably. In .NET (including ASP.NET Core), you can test asynchronous code using popular testing frameworks like xUnit, NUnit, or MSTest. These frameworks provide support for async and await, allowing you to write unit tests for asynchronous methods effectively.

Here, we'll walk through how to unit test asynchronous code using xUnit as the test framework. The same principles apply to other frameworks with slight syntax changes.

1. Basics of Unit Testing Asynchronous Code

In unit testing asynchronous code:

  • The test method itself must be async and return Task.
  • You should await the asynchronous method in your test to ensure it completes.
  • Use mocking libraries like Moq for dependencies (e.g., services, repositories) that perform async operations.

2. Setup for Unit Testing with xUnit and Moq

Before diving into examples, let's assume the following setup:

  • You are using xUnit as the testing framework.
  • You are using Moq for mocking dependencies in your unit tests.

To add xUnit and Moq to your project, install the following NuGet packages:

dotnet add package xunit
dotnet add package Moq

3. Example 1: Testing an Async Method for Fetching Data

Let’s take a simple ProductService class that fetches data from a database asynchronously. We want to write a unit test to verify that the asynchronous method works correctly.

ProductService Example

public class ProductService
{
    private readonly IProductRepository _productRepository;
 
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
 
    public async Task<List<Product>> GetProductsAsync()
    {
        return await _productRepository.GetAllProductsAsync();
    }
}

The ProductService class depends on IProductRepository, which we will mock in the unit test.

IProductRepository Interface

public interface IProductRepository
{
    Task<List<Product>> GetAllProductsAsync();
}

Unit Test for GetProductsAsync Using xUnit and Moq

  1. Create the Unit Test Class
using Moq;
using Xunit;
using System.Collections.Generic;
using System.Threading.Tasks;
 
public class ProductServiceTests
{
    [Fact] // This attribute marks the method as a unit test
    public async Task GetProductsAsync_ReturnsProducts()
    {
        // Arrange
        var mockProductRepo = new Mock<IProductRepository>();
 
        // Mock the repository to return a predefined list of products asynchronously
        mockProductRepo.Setup(repo => repo.GetAllProductsAsync())
            .ReturnsAsync(new List<Product>
            {
                new Product { Id = 1, Name = "Product 1", Price = 10 },
                new Product { Id = 2, Name = "Product 2", Price = 20 }
            });
 
        var productService = new ProductService(mockProductRepo.Object);
 
        // Act
        var result = await productService.GetProductsAsync();
 
        // Assert
        Assert.NotNull(result); // Ensure result is not null
        Assert.Equal(2, result.Count); // Ensure we got two products
        Assert.Equal("Product 1", result[0].Name); // Ensure product details match
    }
}

Explanation of the Test:

  • Arrange: A mock object (mockProductRepo) is created for the IProductRepository interface. We set up this mock to return a predefined list of products asynchronously when GetAllProductsAsync() is called.
  • Act: We invoke the GetProductsAsync method on the ProductService and await its result.
  • Assert: We verify that the returned list of products is not null and contains the expected number and details of products.

4. Example 2: Testing an Async Method for Saving Data

Now let’s test an asynchronous method that inserts data into the database. This method will simulate saving a product asynchronously.

ProductService Example

public class ProductService
{
    private readonly IProductRepository _productRepository;
 
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
 
    public async Task AddProductAsync(Product product)
    {
        await _productRepository.AddProductAsync(product);
    }
}

IProductRepository Interface

public interface IProductRepository
{
    Task AddProductAsync(Product product);
}

Unit Test for AddProductAsync

  1. Create the Unit Test Class
public class ProductServiceTests
{
    [Fact]
    public async Task AddProductAsync_CallsRepositoryOnce()
    {
        // Arrange
        var mockProductRepo = new Mock<IProductRepository>();
        var productService = new ProductService(mockProductRepo.Object);
        var newProduct = new Product { Id = 1, Name = "New Product", Price = 50 };
 
        // Act
        await productService.AddProductAsync(newProduct);
 
        // Assert
        mockProductRepo.Verify(repo => repo.AddProductAsync(newProduct), Times.Once);
    }
}

Explanation of the Test:

  • Arrange: We create a mock for IProductRepository and pass it to the ProductService. We also create a new Product object to test the insertion.
  • Act: We call AddProductAsync and await the result.
  • Assert: We verify that AddProductAsync was called exactly once on the repository, ensuring the product was added correctly.

5. Example 3: Testing Exception Handling in Async Code

Let’s write a test to ensure that the service throws an exception when something goes wrong during the data fetching process.

ProductService Example

public class ProductService
{
    private readonly IProductRepository _productRepository;
 
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
 
    public async Task<List<Product>> GetProductsAsync()
    {
        return await _productRepository.GetAllProductsAsync();
    }
}

Unit Test for Exception Handling

  1. Create the Unit Test Class
public class ProductServiceTests
{
    [Fact]
    public async Task GetProductsAsync_ThrowsException_WhenRepositoryFails()
    {
        // Arrange
        var mockProductRepo = new Mock<IProductRepository>();
 
        // Mock the repository to throw an exception when called
        mockProductRepo.Setup(repo => repo.GetAllProductsAsync())
            .ThrowsAsync(new Exception("Database connection failed"));
 
        var productService = new ProductService(mockProductRepo.Object);
 
        // Act & Assert
        var exception = await Assert.ThrowsAsync<Exception>(async () =>
            await productService.GetProductsAsync());
 
        // Verify the exception message
        Assert.Equal("Database connection failed", exception.Message);
    }
}

Explanation of the Test:

  • Arrange: We mock the IProductRepository to throw an exception when GetAllProductsAsync is called.
  • Act & Assert: We use Assert.ThrowsAsync to check if the exception is thrown as expected and assert that the exception message matches.

6. Key Points for Testing Asynchronous Code

  • async Unit Tests: The test method should be marked async Task to properly await asynchronous operations.
  • Mocking Dependencies: Use a mocking library like Moq to mock async methods on dependencies (e.g., repositories, services).
  • Assertions for Async Code:
    • Use Assert.ThrowsAsync to verify exceptions in async code.
    • Use await in the test method to properly execute and wait for async methods to complete.

7. Real-Life Analogy

Imagine you have a remote worker who responds asynchronously to your emails. To ensure they are working correctly, you send an email (Arrange), wait for the response (Act), and then check that they’ve responded correctly (Assert). Similarly, when testing asynchronous methods, you write the test, invoke the asynchronous operation, and verify the results.

Conclusion

Unit testing asynchronous code in ASP.NET Core is straightforward with testing frameworks like xUnit and mocking libraries like Moq. By marking your test methods as async Task and using await on asynchronous operations, you can ensure that async code runs correctly and efficiently. This approach ensures that your code is reliable, even when performing non-blocking tasks like database operations or external API calls.