Enhetstestning av HttpClient utan mocks (Svenska (Swedish))

Enhetstestning av HttpClient utan mocks

Saturday, 29 November 2025

//

5 minute read

Inledning

Vid testning av kod som använder HttpClient, det traditionella tillvägagångssättet innebär hån HttpMessageHandler med hjälp av ramar som Moq. Medan detta fungerar, kan det vara verbose, ceremoni-tunga, och ärligt talat lite ful. Det finns ett renare alternativ: att använda DelegatingHandler för att skapa testhanterare som beter sig som verkliga HTTP-slutpunkter.

I det här inlägget ska jag visa dig varför du kan hoppa över mocks helt och använda DelegatingHandler för mer läsbar, underhållsbar och kompakt testkod.

Problemet med Mocking HttpMessageHandler

Här är vad typiskt HttpMessageHandler hånande 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);

Detta har flera frågor:

  1. Verbose Ordförande - Massor av pannplatta för vad som ska vara enkelt beteende
  2. Protected method cermony (skyddad metod) - Du behöver Protected() och ItExpr eftersom SendAsync är skyddad
  3. Svårläst - Den verkliga testlogiken är begravd i installationsceremonin.
  4. Kan inte återanvändas - Varje test behöver liknande inställningskod
  5. Britta Ordförande - Lätt att få det strängbaserade metodnamnet fel

Alternativet DelegatingHandler

DelegatingHandler är en inbyggd .NET-klass utformad för just detta ändamål - fånga HTTP-förfrågningar innan de träffar nätverket. Det är vad middleware som retry-hanterare, logghanterare och autentiseringshanterare använder i produktionen.

Här är samma funktionalitet med hjälp av 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")
        };
    }
}

Användning av den:

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

Inga hånfulla ramar, ingen skyddad metodgymnastik, inga strängbaserade metodnamn.

Ett exempel från Real-World: Översättningstjänsthandläggare

Här är ett mer sofistikerat exempel från en översättningstjänst testhanterare:

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)
        };
    }
}

Denna handläggare:

  • Vägar olika vägar till olika beteenden
  • Deserializes begär innehåll för att fatta beslut
  • Returnerar lämpliga felkoder för specifika scenarier
  • Är helt läsbar och självdokumenterande

Förberedelse med beroendeinjektion

Vid användning IHttpClientFactory (som du bör vara), integrera testhanterare ä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"));
}

Avancerat mönster: Konfigurerbara handtag

För mer flexibilitet kan du skapa hanterare som accepterar konfiguration:

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 på Moq-baserad Mocking och DelegatingHandler |--------|-------------------|-------------------| | Kodrader på många, många, få, | Läsbarhet Högt (bara C#) | Återanvändbarhet "Stackars" Utmärkt | Felsökning med hårdare (mock magic) på ett enkelt sätt (steg igenom) | Tillverkning och Brittle och Robust | Lärandekurva till Steeper (Moq API:er) på ett minimalt sätt | Beroenden på Kräver Moq och ingen (inbyggd) till

När mocking fortfarande gör sig förstådd

För att vara rättvis, det finns scenarier där Moq-stil hån fortfarande kan vara lämpligt:

  1. Engångs enkla svar - Om du behöver en enda svarshanterare en gång, kan inline Moq vara snabbare
  2. Kontroll - Moq's Verify() är användbart för att hävda att samtal har gjorts
  3. Befintlig kodbas - Om ditt team redan har omfattande Moq infrastruktur

För verifiering 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));
    }
}

Slutsatser

Användning DelegatingHandler För HttpClient test ger dig:

  • Kompakt kod - Inget hån mot ramceremonin.
  • Läsbara tester - Bara vanliga C# lektioner.
  • Återanvändbara handtag - Dela över testklasser
  • Lätt avlusning - Ställ in brytpunkter, steg genom kod
  • Nollberoenden - Den är inbyggd i .NET.

Nästa gång du sträcker dig efter Mock<HttpMessageHandler>, överväga om en enkel DelegatingHandler skulle tjäna dig bättre. Ditt framtida jag (och dina lagkamrater) kommer att tacka dig för renare, mer underhållbara testkod.

Se testprojekten i denna lösning för verkliga exempel på detta mönster i praktiken.

Finding related posts...
logo

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