How to unit test async methods in C#
By FoxLearn 1/22/2025 3:07:25 AM 4
In this article, we’ll explore how to unit test async methods, focusing on testing a method that interacts with external dependencies.
Simple Async Method Example
Let’s start with a basic asynchronous method. Suppose we have a method that simulates fetching data from a database and returns a user’s details by ID:
public async Task<User> GetUserAsync(int userId) { // Simulate a database call (e.g., with Entity Framework or ADO.NET) return await Task.FromResult(new User { Id = userId, Name = "John Doe" }); }
Here’s a unit test for this simple method:
[TestMethod] public async Task GetUserAsync_WhenCalled_ReturnsUser() { // Arrange var expectedUser = new User { Id = 1, Name = "John Doe" }; // Act var actualUser = await userService.GetUserAsync(1); // Assert Assert.AreEqual(expectedUser.Id, actualUser.Id); Assert.AreEqual(expectedUser.Name, actualUser.Name); }
In this example, GetUserAsync
is a straightforward method that we can test directly. However, when asynchronous methods involve external dependencies (e.g., database queries, API calls), we need to mock those dependencies in our tests.
Real-World Scenario: Testing a Service That Fetches Data from a Web API
Let’s now consider a real-world scenario where we want to test a service that fetches data from an external web API. This will involve more complex asynchronous behavior, as the service interacts with an external system.
We’ll build a class, WeatherService
, which fetches weather data asynchronously from an API. We want to unit test this class without actually calling the external API.
The WeatherService Class
public class WeatherService { private readonly IWeatherApiClient _weatherApiClient; public WeatherService(IWeatherApiClient weatherApiClient) { _weatherApiClient = weatherApiClient; } public async Task<WeatherData> GetWeatherAsync(string city) { var weatherResponse = await _weatherApiClient.FetchWeatherDataAsync(city); return new WeatherData { City = city, Temperature = weatherResponse.Temperature, Condition = weatherResponse.Condition }; } }
In this example, WeatherService
depends on an external service, IWeatherApiClient
, to fetch weather data. To make this class testable, we’ll inject an implementation of IWeatherApiClient
via the constructor.
The IWeatherApiClient Interface
public interface IWeatherApiClient { Task<WeatherApiResponse> FetchWeatherDataAsync(string city); }
This interface defines the FetchWeatherDataAsync
method, which the WeatherService
relies on to fetch weather data. During unit testing, we will mock this interface to simulate fetching weather data without actually calling an API.
WeatherData and WeatherApiResponse Classes
public class WeatherData { public string City { get; set; } public double Temperature { get; set; } public string Condition { get; set; } } public class WeatherApiResponse { public double Temperature { get; set; } public string Condition { get; set; } }
The WeatherData
class represents the processed weather information that WeatherService
will return, and WeatherApiResponse
is the data format returned by the external API.
Unit Testing the WeatherService Class
Now that we have our WeatherService
and IWeatherApiClient
in place, let’s look at how to write a unit test for GetWeatherAsync
. The goal is to mock the external API call so that we don’t make a real HTTP request.
We’ll use the Moq framework to mock the IWeatherApiClient
.
[TestMethod] public async Task GetWeatherAsync_WhenCityIsValid_ReturnsCorrectWeatherData() { // Arrange var city = "New York"; var expectedWeatherData = new WeatherData { City = city, Temperature = 22.5, Condition = "Sunny" }; var mockWeatherApiClient = new Mock<IWeatherApiClient>(); mockWeatherApiClient .Setup(client => client.FetchWeatherDataAsync(city)) .ReturnsAsync(new WeatherApiResponse { Temperature = 22.5, Condition = "Sunny" }); var weatherService = new WeatherService(mockWeatherApiClient.Object); // Act var actualWeatherData = await weatherService.GetWeatherAsync(city); // Assert Assert.AreEqual(expectedWeatherData.City, actualWeatherData.City); Assert.AreEqual(expectedWeatherData.Temperature, actualWeatherData.Temperature); Assert.AreEqual(expectedWeatherData.Condition, actualWeatherData.Condition); }
By mocking the IWeatherApiClient
dependency, we can focus on testing the logic within WeatherService
without worrying about the external API or network calls.
The Moq framework allows us to easily simulate asynchronous method calls and define the behavior of mock objects using the ReturnsAsync
method. This ensures that our tests can simulate real-world scenarios without depending on external systems.
Unit testing async methods in C# is made easier when you understand how to mock external dependencies and isolate the logic you're testing. In the example of WeatherService
, we used dependency injection to inject a mock of the IWeatherApiClient
, allowing us to simulate an external API call.
- Dictionary with multiple values per key in C#
- How to start, stop and verify if a service exists in C#
- C# Async/await with a Func delegate
- C# Async Main
- Fixing the Sync over Async Antipattern in C#
- How to Modify app.config at Runtime
- SqlTypeException: SqlDateTime overflow
- SqlTypeException: 'Overflow error converting to data type int.'