# Yksikön testaus HttpClient ilman pukkeja

<datetime class="hidden">2025-11-29T07:00</datetime>

<!--category-- xUnit, Unit Testing, HttpClient -->
## Johdanto

Testattaessa koodia, joka käyttää `HttpClient`Perinteiseen 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.

[TOC]

## Ongelma Mocking HttpMessageHandlerissa

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

```csharp
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`:

```csharp
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ä:

```csharp
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ä:

```csharp
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:

```csharp
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:

```csharp
[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:

```csharp
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ö:

```csharp
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:

```csharp
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ä.