# Unit Testing HttpClient ZONDER Mocks

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

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

Bij het testen van code die gebruik maakt van `HttpClient`, de traditionele aanpak houdt bespotting in `HttpMessageHandler` met behulp van kaders zoals Moq. Terwijl dit werkt, kan het werkbose, ceremonie-zware, en eerlijk gezegd een beetje lelijk. Er is een schoner alternatief: gebruik `DelegatingHandler` om testverwerkers te maken die zich gedragen als echte HTTP-eindpunten.

In dit bericht zal ik laten zien waarom je zou kunnen overslaan de mocks volledig en gebruik `DelegatingHandler` voor meer leesbaar, onderhoudbaar en compacte testcode.

[TOC]

## Het probleem met Mocking HttpMessageHandler

Dit is wat typisch is. `HttpMessageHandler` spotten lijkt op met 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);
```

Dit heeft verschillende kwesties:

1. **Verbose** - Veel ketelplaat voor wat eenvoudig gedrag zou moeten zijn
2. **Protected method ceremonion** - Je moet `Protected()` en `ItExpr` omdat `SendAsync` is beschermd
3. **Moeilijk te lezen** - De werkelijke test logica is begraven in setup ceremonie
4. **Niet herbruikbaar** - Elke test heeft een vergelijkbare setup code nodig
5. **Brittle** - Makkelijk om de op string gebaseerde methodenaam verkeerd te krijgen

## De Verwijderende Handler Alternatief

`DelegatingHandler` is een ingebouwde .NET klasse ontworpen voor precies dit doel - het onderscheppen van HTTP-verzoeken voordat ze het netwerk raken. Het is wat middleware zoals retry handlers, logging handlers, en authenticatie handlers gebruiken in de productie.

Hier is dezelfde functionaliteit met behulp van `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")
        };
    }
}
```

Met behulp van het:

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

Geen bespotting kaders, geen beschermde methode gymnastiek, geen op string gebaseerde methode namen.

## Een Real-World Voorbeeld: Vertaaldienst Handler

Hier is een meer verfijnd voorbeeld van een vertaaldienst test handler:

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

Deze begeleider:

- Routes verschillende paden naar verschillende gedragingen
- Deserialiseert verzoeken om inhoud om beslissingen te nemen
- Geeft passende foutcodes voor specifieke scenario's terug
- Is volledig leesbaar en zelf documenteren

## Opzetten met Afhankelijkheidsinjectie

Bij gebruik `IHttpClientFactory` (wat je zou moeten zijn), het integreren van testverwerkers is eenvoudig:

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

Dan in uw tests:

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

## Geavanceerd patroon: instelbare handlers

Voor meer flexibiliteit kunt u handlers creëren die configuratie accepteren:

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

Gebruik:

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

## Waarom kiezen VerwijderenHandler Over Mocks?

Mocking op basis van Moq DelegingHandler
|--------|-------------------|-------------------|
| **Coderegels** Veel weinig
| **Leesbaarheid** Laag (ceremonie zwaar) Hoog (gewoon C#)
| **Herbruikbaarheid** Uitstekend.
| **Debuggen** Harder (mock magie) . . Eenvoudig (stap door) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
| **Refactoring** Brettle Robuust
| **Leercurve** Steeper (Moq API's)
| **Afhankelijkheden** Moq vereist geen (ingebouwd)

## Wanneer Spoten nog steeds maakt gevoel

Om eerlijk te zijn, zijn er scenario's waarin Moq-stijl spotten nog steeds geschikt zou kunnen zijn:

1. **Eenmalige eenvoudige antwoorden** - Als u eenmaal een enkele responsverlener nodig heeft, kan inline Moq sneller zijn
2. **Verificatie** - Moq's `Verify()` is nuttig voor het bevestigen van oproepen werden gedaan
3. **Bestaande codebasis** - Als uw team al uitgebreide Moq infrastructuur heeft

Voor verificatie, kunt u het toevoegen aan VerwijderingHandler ook:

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

## Conclusie

Gebruik `DelegatingHandler` voor HttpClient testen geeft u:

- **Compacte code** - Geen bespotting kader ceremonie
- **Leesbare tests** - Gewoon gewone C# klassen.
- **Herbruikbare verwerkers** - Delen over de testklassen
- **Eenvoudig debuggen** - Stel breekpunten in, stap door code
- **Nul afhankelijkheden** - Het is ingebouwd in .NET

De volgende keer dat je `Mock<HttpMessageHandler>`, na te gaan of een eenvoudige `DelegatingHandler` Je toekomstige zelf (en je teamgenoten) zal je dankbaar zijn voor de schonere, duurzamere testcode.

Zie de testprojecten in deze oplossing voor real-world voorbeelden van dit patroon in actie.