# Unit Testing HttpClient OHNE Mocks

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

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

Beim Testen von Code, der verwendet wird `HttpClient`, der traditionelle Ansatz beinhaltet Spott `HttpMessageHandler` mit Frameworks wie Moq. Während dies funktioniert, kann es verbal sein, Zeremonie-schwer, und offen gesagt ein bisschen hässlich. Es gibt eine saubere Alternative: mit `DelegatingHandler` um Testhandler zu erstellen, die sich wie echte HTTP-Endpunkte verhalten.

In diesem Beitrag werde ich Ihnen zeigen, warum Sie die Spott ganz überspringen und verwenden `DelegatingHandler` für lesbareren, haltbareren und kompakteren Testcode.

[TOC]

## Das Problem mit Mocking HttpMessageHandler

Hier ist, was typisch ist `HttpMessageHandler` Spott sieht aus wie mit 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);
```

Dies hat mehrere Probleme:

1. **Verbiegung** - Viele Kesselplatten für ein einfaches Verhalten
2. **Geschützte Methode Zeremonie** - Du brauchst `Protected()` und `ItExpr` weil `SendAsync` geschützt ist
3. **Schwer zu lesen** - Die eigentliche Testlogik ist in der Setup-Zeremonie begraben
4. **Nicht wiederverwendbar** - Jeder Test benötigt einen ähnlichen Setup-Code
5. **Bruch** - Einfach den String-basierten Methodennamen falsch zu bekommen

## Die delegierende Handler-Alternative

`DelegatingHandler` ist eine eingebaute .NET-Klasse, die genau für diesen Zweck entwickelt wurde - das Abfangen von HTTP-Anfragen, bevor sie das Netzwerk treffen. Es ist, was Middleware wie retry handlers, Protokoll-Handler und Authentifizierung handlers in der Produktion verwenden.

Hier ist die gleiche Funktionalität mit `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")
        };
    }
}
```

Verwendung:

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

Keine spöttischen Frameworks, keine geschützten Methodenturnen, keine String-basierten Methodennamen.

## Ein Beispiel für die reale Welt: Übersetzungsdienst Handler

Hier ist ein anspruchsvolleres Beispiel aus einem Translation Service 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)
        };
    }
}
```

Dieser Handler:

- Wege zu unterschiedlichen Verhaltensweisen
- Deserialisiert Anfragen Inhalt zu Entscheidungen zu treffen
- Gibt entsprechende Fehlercodes für spezifische Szenarien zurück
- Ist vollständig lesbar und selbstdokumentiert

## Einrichten mit Abhängigkeitsinjektion

Bei der Anwendung `IHttpClientFactory` (was Sie sein sollten) ist die Integration von Testhandlern einfach:

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

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

## Erweitertes Muster: Konfigurierbare Handler

Für mehr Flexibilität können Sie Handler erstellen, die die Konfiguration akzeptieren:

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

Verwendung:

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

## Warum wählen Sie DelegatingHandler über Mocks?

Benachteiligtes Moq-basiertes Mocking, Delegieren von Handler
|--------|-------------------|-------------------|
| **Codezeilen** Viele wenige
| **Lesbarkeit** Tief (Zeremonie schwer) Hoch (nur C#)
| **Wiederverwendbarkeit** . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
| **Debugging** Härter (Mockmagie) Einfach (Schritt durch)
| **Refaktorisierung** . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
| **Lernkurve** Steeper (Moq-APIs)
| **Abhängigkeiten** Benötigt keine (eingebauten)

## Wenn Mocking immer noch Sinn macht

Um fair zu sein, gibt es Szenarien, in denen Moq-Stil Spott noch angemessen sein könnte:

1. **Einmalige einfache Antworten** - Wenn Sie einmal einen Single-Response-Handler benötigen, könnte Inline Moq schneller sein
2. **Überprüfung** - Moq's `Verify()` ist nützlich für die Behauptung, Anrufe wurden gemacht
3. **Bestehende Codebasis** - Wenn Ihr Team bereits über umfangreiche Moq-Infrastruktur

Zur Überprüfung können Sie es auch dem DelegatingHandler hinzufügen:

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

## Schlußfolgerung

Verwendung `DelegatingHandler` für HttpClient-Tests gibt Ihnen:

- **Kompakter Code** - Keine Verspottung Rahmen Zeremonie
- **Lesbare Tests** - Nur normale C#-Kurse
- **Wiederverwendbare Handler** - Anteil über die Testklassen
- **Einfaches Debuggen** - Haltepunkte setzen, Code durchtreten
- **Null-Abhängigkeiten** - Es ist in .NET eingebaut.

Das nächste Mal, wenn Sie nach `Mock<HttpMessageHandler>`, prüfen, ob eine einfache `DelegatingHandler` Ihr zukünftiges Selbst (und Ihre Teamkollegen) wird Ihnen für den saubereren, wartungsfähigeren Testcode danken.

Siehe die Testprojekte in dieser Lösung für reale Beispiele dieses Musters in Aktion.