Back to "Unit Testing HttpClient utan Mocks"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

HttpClient Unit Testing xUnit

Unit Testing HttpClient utan Mocks

Saturday, 29 November 2025

Inleiding

När man testar kod som använder HttpClient, det traditionella sättet innebär att skämta HttpMessageHandler med hjälp av plattformar som Moq. DelegatingHandler att skapa tester som beter sig som riktiga HTTP slutpunkter.

I denna artikel ska jag visa er varför ni kan hoppa över modellerna helt och använda DelegatingHandler för mer läsbar

Problemet med att kopiera HttpMessageHandler

Här är vad som typiskt HttpMessageHandler mocking ser ut som med Moq:

var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.ToString().Contains("api/send")),
        ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync((HttpRequestMessage request, CancellationToken cancellationToken) =>
    {
        var requestBody = request.Content?.ReadAsStringAsync(cancellationToken).Result;
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(requestBody ?? "No content", Encoding.UTF8, "application/json")
        };
    });

var client = new HttpClient(mockHandler.Object);

Det har flera problem.

  1. Verbose - Många boilerplattor för vad som borde vara ett enkelt beteende
  2. Protected method ceremoni - Du behöver Protected() och ItExpr eftersom SendAsync är skyddad
  3. Svårt att läsa - Den faktiska testlogiken är begravd i inställningssceremonin
  4. Inte återanvändbar - Varje test behöver samma konfigurationskod
  5. Brittle - Det är lätt att få tag i stringen-baserade metodens namn fel

DelegeringHandler alternativet

DelegatingHandler är en inbyggd NET-klass, designad precis för den här syften. - tar emot HTTP- begäran innan de träffar nätverket.

Här ' har samma funktion med DelegatingHandler:

public class EchoHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var content = request.Content != null
            ? await request.Content.ReadAsStringAsync(cancellationToken)
            : "No content";

        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(content, Encoding.UTF8, "application/json")
        };
    }
}

Att använda den:

var client = new HttpClient(new EchoHandler());

Det "'" är det "." Ingen mocking ramverk , , ingen skyddad metod gymnastik , , inga strängar, - , med metoder som baseras på namn, .

Ett verkligt-World Example: Translation Service Handler

Här är ett mer sofistikerat exempel från en testbehandlare för översättningstjänster.

public class TranslateDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var absPath = request.RequestUri?.AbsolutePath;
        var method = request.Method;

        return absPath switch
        {
            "/translate" when method == HttpMethod.Post => await HandleTranslate(request),
            "/translate" => new HttpResponseMessage(HttpStatusCode.OK),
            "/health" => new HttpResponseMessage(HttpStatusCode.OK),
            _ => new HttpResponseMessage(HttpStatusCode.NotFound)
        };
    }

    private static async Task<HttpResponseMessage> HandleTranslate(HttpRequestMessage request)
    {
        var content = await request.Content!.ReadFromJsonAsync<TranslateRequest>();

        // Simulate error for specific test case
        if (content?.TargetLanguage == "xx")
            return new HttpResponseMessage(HttpStatusCode.InternalServerError);

        var response = new TranslateResponse("es", new[] { "Texto traducido" });
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(response)
        };
    }
}

Den här handledaren:

  • Routerar olika vägar till olika beteenden
  • Deserialiserar begäran innehåll för att fatta beslut
  • Ge tillbaka lämpliga felkoder för specifika scenarior
  • Är fullständigt läsbar och självdokumenterar

Konfiguration av beroende injektion

När man använder IHttpClientFactory (där du borde vara ), integrerande testbehandlare är enkelt

public static IServiceCollection SetupTestServices(DelegatingHandler handler)
{
    var services = new ServiceCollection();

    services.AddHttpClient<ITranslationService, TranslationService>(client =>
    {
        client.BaseAddress = new Uri("https://test.local");
    })
    .ConfigurePrimaryHttpMessageHandler(() => handler);

    return services;
}

Sedan i dina tester:

[Fact]
public async Task Translate_ReturnsTranslatedText()
{
    var services = SetupTestServices(new TranslateDelegatingHandler());
    var provider = services.BuildServiceProvider();
    var service = provider.GetRequiredService<ITranslationService>();

    var result = await service.TranslateAsync("Hello", "es");

    Assert.Equal("Texto traducido", result);
}

[Fact]
public async Task Translate_InvalidLanguage_ThrowsException()
{
    var services = SetupTestServices(new TranslateDelegatingHandler());
    var provider = services.BuildServiceProvider();
    var service = provider.GetRequiredService<ITranslationService>();

    await Assert.ThrowsAsync<HttpRequestException>(
        () => service.TranslateAsync("Hello", "xx"));
}

avancerad mönster : Konfigurerade handler

För mer flexibilitet, kan du skapa handler som accepterar konfigurationen:

public class ConfigurableHandler : DelegatingHandler
{
    private readonly Dictionary<string, Func<HttpRequestMessage, Task<HttpResponseMessage>>> _routes;

    public ConfigurableHandler()
    {
        _routes = new Dictionary<string, Func<HttpRequestMessage, Task<HttpResponseMessage>>>();
    }

    public ConfigurableHandler WithRoute(string path, HttpStatusCode status)
    {
        _routes[path] = _ => Task.FromResult(new HttpResponseMessage(status));
        return this;
    }

    public ConfigurableHandler WithRoute(string path, object responseBody)
    {
        _routes[path] = _ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(responseBody)
        });
        return this;
    }

    public ConfigurableHandler WithRoute(
        string path,
        Func<HttpRequestMessage, Task<HttpResponseMessage>> handler)
    {
        _routes[path] = handler;
        return this;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var path = request.RequestUri?.AbsolutePath ?? "";

        if (_routes.TryGetValue(path, out var handler))
            return await handler(request);

        return new HttpResponseMessage(HttpStatusCode.NotFound);
    }
}

Användning:

var handler = new ConfigurableHandler()
    .WithRoute("/api/users", new[] { new User("Alice"), new User("Bob") })
    .WithRoute("/api/health", HttpStatusCode.OK)
    .WithRoute("/api/error", HttpStatusCode.InternalServerError);

var client = new HttpClient(handler);

Varför välja DelegatingHandler över Mocks?

Aspect Moq-based Mocking DelegatingHandler
Kodlinjer Många Få
Läsbarhet låga ( festligt tungt) högt M(just C
Återanvändbarhet Fattig Utmärkt
Debugging Svårare ( skämt magi ) enkelt ( gå genom
Refactoring Brittle Robust
Lärningskurva Steeper ( Moq APIs ) МSK3 Minimal
beroenden kräver Moq Ingen ( byggt - i )

När mocking fortfarande verkar vettigt

För att vara rättvis, så finns det scenarioer där Moqs mockerning fortfarande kan vara lämpligt.

  1. Ett-lös enkla svar - Om du behöver en enda handlere för ett svar en gång , Moq kan vara snabbare
  2. Bekräftelse - MoqM SK1s Verify() är användbar för att bevisa att samtalen gjorts
  3. Existerande kodbas - Om ditt team redan har en omfattande Moq-infrastruktur

För att verifiera, kan du lägga till den i DelegatingHandler också:

public class VerifyingHandler : DelegatingHandler
{
    public List<HttpRequestMessage> ReceivedRequests { get; } = new();

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        ReceivedRequests.Add(request);
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}

Slutsatsen

Att använda DelegatingHandler för HttpClient-testning ger dig

  • Kompaktkod - Ingen mocking ram ceremoni
  • Läsbara tester - Bara regelbundna CM SK1 klass
  • Handler som kan användas igen - dela genom testklassen
  • Låg dämpning - Set breakpoints , step through code
  • Null beroenden - Det' är inbyggt i .NET

Nästa gång du når för Mock<HttpMessageHandler>, överväg om en enkel DelegatingHandler skulle tjäna dig bättre. ditt framtida själv ( och dina lagkamrater ) kommer tacka dig för den renare

Se testprojektet i denna lösning för verkliga exempel på detta mönster i verkligheten

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.