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
asyncand returnTask. - You should
awaitthe 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 Moq3. 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
- 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 theIProductRepositoryinterface. We set up this mock to return a predefined list of products asynchronously whenGetAllProductsAsync()is called. - Act: We invoke the
GetProductsAsyncmethod on theProductServiceand 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
- 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
IProductRepositoryand pass it to theProductService. We also create a newProductobject to test the insertion. - Act: We call
AddProductAsyncand await the result. - Assert: We verify that
AddProductAsyncwas 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
- 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
IProductRepositoryto throw an exception whenGetAllProductsAsyncis called. - Act & Assert: We use
Assert.ThrowsAsyncto check if the exception is thrown as expected and assert that the exception message matches.
6. Key Points for Testing Asynchronous Code
asyncUnit Tests: The test method should be markedasync Taskto 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.ThrowsAsyncto verify exceptions in async code. - Use
awaitin the test method to properly execute and wait for async methods to complete.
- Use
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.