# Enhetstestning av HttpClient utan mocks

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

<!--category-- xUnit, Unit Testing, HttpClient -->
## 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.

[TOC]

## Problemet med Mocking HttpMessageHandler

Här är vad typiskt `HttpMessageHandler` hånande ser ut som med Moq:

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

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

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

Användning av den:

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

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

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:

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

Sedan i dina tester:

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

## Avancerat mönster: Konfigurerbara handtag

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

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

Användning:

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

## 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å:

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

## 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.