Back to "Yksikön testaus HttpClient ilman pukkeja"

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

Yksikön testaus HttpClient ilman pukkeja

Saturday, 29 November 2025

Johdanto

Testattaessa koodia, joka käyttää HttpClientPerinteiseen lähestymistapaan kuuluu pilkkaaminen HttpMessageHandler Moqin kaltaisia kehyksiä käyttäen. Vaikka tämä toimii, se voi olla sanavalmis, seremoniapainotteinen ja suoraan sanottuna hieman ruma. On olemassa puhtaampi vaihtoehto: käyttämällä DelegatingHandler Luoda testikäsittelijöitä, jotka käyttäytyvät todellisten HTTP-päätteiden mukaisesti.

Tässä viestissä näytän, miksi voit jättää mokat kokonaan väliin ja käyttää DelegatingHandler Lisää luettavaa, ylläpidettävää ja kompaktia testikoodia.

Ongelma Mocking HttpMessageHandlerissa

Tämä on tyypillistä HttpMessageHandler pilkkaaminen näyttää Moqin kanssa:

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

Tässä on useita ongelmia:

  1. Verboosi - Paljon pannulautasta yksinkertaiseen käytökseen
  2. Suojattu menetelmäseremonia -Tarvitset apua. Protected() sekä ItExpr koska SendAsync on suojattu
  3. Vaikeasti luettavaa - Varsinainen testilogiikka on haudattu seremonioihin
  4. Ei uudelleenkäytettävissä - Jokainen testi tarvitsee samanlaisen asetuskoodin
  5. Hento - Helppo saada merkkijonopohjainen nimi väärin

Delegoiva handler-vaihtoehto

DelegatingHandler Se on sisäänrakennettu .NET-luokka, joka on suunniteltu juuri tähän tarkoitukseen - siepaten HTTP-pyynnöt ennen kuin ne osuvat verkkoon. Sitä keskiohjelmistot, kuten uudelleenryöstäjät, puunkäsittelyn käsittelijät ja todentamisen käsittelijät, käyttävät tuotannossa.

Tässä on sama toiminnallisuus 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")
        };
    }
}

Käyttämällä sitä:

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

Ei pilkkakehyksiä, ei suojattua metodin voimistelua, ei narupohjaisia nimiä.

Reaalimaailman esimerkki: käännöspalvelu Handler

Tässä on hienostuneempi esimerkki käännöspalvelun testikäsittelijältä:

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

Tämä käsittelijä:

  • Reitit eri polkuja eri käyttäytymiseen
  • Haluaa pyytää sisältöä tekemään päätöksiä
  • Palauttaa asianmukaiset virhekoodit tiettyjä skenaarioita varten
  • On täysin luettavissa ja itse dokumentoitavissa

Riippuvuusruiskeen antaminen

Käytettäessä IHttpClientFactory Testien käsittelijöiden integroiminen on yksinkertaista:

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

Sitten testeissäsi:

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

Advanced Pattern: Luotettavat kahvat

Joustavuuden lisäämiseksi voit luoda käsittelijöitä, jotka hyväksyvät kokoonpanon:

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

Käyttö:

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

Miksi valita hirsipuikkojen sijasta delegointi?

Moq-pohjainen Mocking-delegointiHandler-delegointi |--------|-------------------|-------------------| | Koodirivit Monia harvalukuisia | Luettavuus Alhainen (seremonia raskas) Korkea (vain C#) | Uudelleenkäytettävyys Köyhät loistavat | Vianetsintä Kovempaa (mock magic) Helppoa (step through) | Uusintakerroin "Lahjus ryöppyää" | Oppimiskäyrä Steeper (Moqin rajapinnat) Minimaaliset rajapinnat | Riippuvuussuhteet Vaatii Moq None (sisäänrakennettu)

Kun nuuskiminen tekee yhä järkeväksi

Ollakseni reilu, on olemassa skenaarioita, joissa moq-tyylinen pilkkaaminen voisi edelleen olla tarkoituksenmukaista:

  1. Kertaluontoiset yksinkertaiset vastaukset - Jos tarvitset yhden vasteen käsittelijän kerran, Moq saattaa olla nopeampi
  2. Varmistus - Moq's Verify() On hyödyllistä vakuuttaa puheluita soitettiin
  3. Olemassa oleva koodipohja - Jos tiimilläsi on jo laaja Moq-infrastruktuuri

Varmennusta varten voit lisätä sen myös DelegatingHandleriin:

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

Päätelmät

Käyttäminen DelegatingHandler HttpClient-testauksesta saat seuraavat tiedot:

  • Kompakti koodi - Ei pilkkaavaa seremoniaa
  • Luettavat testit - Vain tavallisia C#-kursseja.
  • Uudelleenkäytettävät käsittelijät - Osuus testiluokista
  • Helppo vianetsintä - Aseta keskeytyspisteet, astu koodin läpi
  • Ei riippuvuuksia - Se on sisäänrakennettu NETiin.

Seuraavalla kerralla kurotat Mock<HttpMessageHandler>, harkitse, onko yksinkertainen DelegatingHandler Tuleva minäsi (ja joukkuetoverisi) kiittää sinua puhtaammasta, huollettavammasta testikoodista.

Katso tämän ratkaisun testihankkeista tosimaailman esimerkkejä tästä kuviosta käytännössä.

logo

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