How to unit test async methods in C#

By FoxLearn 1/22/2025 3:07:25 AM   4
Unit testing asynchronous methods in C# is an essential skill for ensuring your code functions as expected, especially when working with external dependencies like web APIs or databases.

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.