How do I use HttpClient (C#) in a unit test?

Testing code that uses HttpClient requires mocking HTTP responses rather than making actual network calls. This ensures unit tests are fast, reliable, and don't depend on external services. There are several approaches to achieve this in C#.

Why Mock HttpClient?

Unit tests should: - Run quickly without network delays - Be deterministic and not fail due to network issues - Not depend on external services being available - Allow testing various response scenarios (success, failure, timeouts)

Method 1: Mocking HttpMessageHandler with Moq

This is the most common approach for mocking HttpClient behavior.

Required NuGet Packages

<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />

Complete Working Example

using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;

// Service class to be tested
public class UserService
{
    private readonly HttpClient _httpClient;

    public UserService(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task<User> GetUserAsync(int userId)
    {
        var response = await _httpClient.GetAsync($"/users/{userId}");
        response.EnsureSuccessStatusCode();

        var content = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<User>(content, new JsonSerializerOptions 
        { 
            PropertyNameCaseInsensitive = true 
        });
    }

    public async Task<bool> CreateUserAsync(User user)
    {
        var json = JsonSerializer.Serialize(user);
        var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync("/users", content);
        return response.IsSuccessStatusCode;
    }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

// Unit tests
public class UserServiceTests : IDisposable
{
    private readonly Mock<HttpMessageHandler> _mockHandler;
    private readonly HttpClient _httpClient;
    private readonly UserService _userService;

    public UserServiceTests()
    {
        _mockHandler = new Mock<HttpMessageHandler>();
        _httpClient = new HttpClient(_mockHandler.Object)
        {
            BaseAddress = new Uri("https://api.example.com/")
        };
        _userService = new UserService(_httpClient);
    }

    [Fact]
    public async Task GetUserAsync_ValidUserId_ReturnsUser()
    {
        // Arrange
        var expectedUser = new User { Id = 1, Name = "John Doe", Email = "john@example.com" };
        var jsonResponse = JsonSerializer.Serialize(expectedUser);

        _mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.Is<HttpRequestMessage>(req => 
                    req.Method == HttpMethod.Get && 
                    req.RequestUri.ToString().EndsWith("/users/1")),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent(jsonResponse, System.Text.Encoding.UTF8, "application/json")
            });

        // Act
        var result = await _userService.GetUserAsync(1);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedUser.Id, result.Id);
        Assert.Equal(expectedUser.Name, result.Name);
        Assert.Equal(expectedUser.Email, result.Email);

        // Verify the HTTP call was made correctly
        _mockHandler.Protected().Verify(
            "SendAsync",
            Times.Once(),
            ItExpr.Is<HttpRequestMessage>(req =>
                req.Method == HttpMethod.Get &&
                req.RequestUri.ToString().EndsWith("/users/1")),
            ItExpr.IsAny<CancellationToken>());
    }

    [Fact]
    public async Task CreateUserAsync_ValidUser_ReturnsTrue()
    {
        // Arrange
        var newUser = new User { Name = "Jane Doe", Email = "jane@example.com" };

        _mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.Is<HttpRequestMessage>(req => 
                    req.Method == HttpMethod.Post && 
                    req.RequestUri.ToString().EndsWith("/users")),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.Created
            });

        // Act
        var result = await _userService.CreateUserAsync(newUser);

        // Assert
        Assert.True(result);
    }

    [Fact]
    public async Task GetUserAsync_UserNotFound_ThrowsHttpRequestException()
    {
        // Arrange
        _mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.NotFound
            });

        // Act & Assert
        await Assert.ThrowsAsync<HttpRequestException>(() => _userService.GetUserAsync(999));
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
        _mockHandler?.Object?.Dispose();
    }
}

Method 2: Using HttpClientFactory with Dependency Injection

For modern .NET applications using IHttpClientFactory, you can create more realistic tests.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using System.Net.Http;

public class UserServiceWithFactory
{
    private readonly IHttpClientFactory _httpClientFactory;

    public UserServiceWithFactory(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<User> GetUserAsync(int userId)
    {
        using var client = _httpClientFactory.CreateClient("UserApi");
        var response = await client.GetAsync($"/users/{userId}");
        response.EnsureSuccessStatusCode();

        var content = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<User>(content, new JsonSerializerOptions 
        { 
            PropertyNameCaseInsensitive = true 
        });
    }
}

public class UserServiceFactoryTests
{
    [Fact]
    public async Task GetUserAsync_WithHttpClientFactory_ReturnsUser()
    {
        // Arrange
        var services = new ServiceCollection();
        var mockHandler = new Mock<HttpMessageHandler>();

        services.AddHttpClient("UserApi", client =>
        {
            client.BaseAddress = new Uri("https://api.example.com/");
        }).ConfigurePrimaryHttpMessageHandler(() => mockHandler.Object);

        var serviceProvider = services.BuildServiceProvider();
        var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
        var userService = new UserServiceWithFactory(httpClientFactory);

        var expectedUser = new User { Id = 1, Name = "John Doe", Email = "john@example.com" };
        var jsonResponse = JsonSerializer.Serialize(expectedUser);

        mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent(jsonResponse, System.Text.Encoding.UTF8, "application/json")
            });

        // Act
        var result = await userService.GetUserAsync(1);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedUser.Name, result.Name);
    }
}

Testing Error Scenarios

Always test how your code handles various HTTP error conditions:

[Theory]
[InlineData(HttpStatusCode.BadRequest)]
[InlineData(HttpStatusCode.Unauthorized)]
[InlineData(HttpStatusCode.Forbidden)]
[InlineData(HttpStatusCode.InternalServerError)]
public async Task GetUserAsync_HttpError_ThrowsHttpRequestException(HttpStatusCode statusCode)
{
    // Arrange
    _mockHandler.Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(new HttpResponseMessage
        {
            StatusCode = statusCode
        });

    // Act & Assert
    await Assert.ThrowsAsync<HttpRequestException>(() => _userService.GetUserAsync(1));
}

[Fact]
public async Task GetUserAsync_Timeout_ThrowsTaskCanceledException()
{
    // Arrange
    _mockHandler.Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>())
        .ThrowsAsync(new TaskCanceledException("Request timed out"));

    // Act & Assert
    await Assert.ThrowsAsync<TaskCanceledException>(() => _userService.GetUserAsync(1));
}

Best Practices

  1. Always dispose HttpClient: Use using statements or implement IDisposable in test classes
  2. Test both success and failure scenarios: Include tests for various HTTP status codes
  3. Verify request details: Check that the correct HTTP method, URL, and headers are used
  4. Use realistic test data: Mirror actual API responses in your mock data
  5. Consider using HttpClientFactory: For dependency injection scenarios
  6. Mock specific endpoints: Use ItExpr.Is<HttpRequestMessage>() to match specific requests
  7. Test timeout scenarios: Ensure your code handles network timeouts gracefully

This approach gives you complete control over HTTP responses in your unit tests while ensuring your code behaves correctly under various network conditions.

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon