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 returnTask
. - 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
- 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 theIProductRepository
interface. We set up this mock to return a predefined list of products asynchronously whenGetAllProductsAsync()
is called. - Act: We invoke the
GetProductsAsync
method on theProductService
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
- 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 theProductService
. We also create a newProduct
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
- 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 whenGetAllProductsAsync
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 markedasync 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.
- 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.