# Unità di prova HttpClient SENZA Mock

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

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

Quando si testa il codice che utilizza `HttpClient`, l'approccio tradizionale comporta la derisione `HttpMessageHandler` usando framework come Moq. Mentre questo funziona, può essere verbose, cerimonia-pesante, e francamente un po 'brutto. C'è un'alternativa più pulita: utilizzando `DelegatingHandler` per creare soggetti che si comportano come veri e propri endpoint HTTP.

In questo post vi mostrerò perché si potrebbe saltare i morsi del tutto e utilizzare `DelegatingHandler` per un codice di prova più leggibile, manutenibile e compatto.

[TOC]

## Il problema con Mocking HttpMessageHandler

Ecco il tipico `HttpMessageHandler` scherzo sembra come con 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);
```

Questo ha diverse questioni:

1. **Verboso** - Un sacco di piastra caldaia per quello che dovrebbe essere semplice comportamento
2. **Cerimonia di metodi protetti** - Hai bisogno di `Protected()` e `ItExpr` perché `SendAsync` è protetto
3. **Difficile da leggere** - La logica del test è sepolta in una cerimonia di messa a punto
4. **Non riutilizzabile** - Ogni test ha bisogno di un codice di configurazione simile
5. **BrittleCity name (optional, probably does not need a translation)** - Facile sbagliare il nome del metodo basato sulle stringhe

## L'alternativa del delegatoHandler

`DelegatingHandler` è una classe .NET integrata progettata proprio per questo scopo - intercettare le richieste HTTP prima di colpire la rete. E 'quello che middleware come i gestori di riprova, i gestori di registrazione e i gestori di autenticazione utilizzano nella produzione.

Qui c'è la stessa funzionalità usando `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")
        };
    }
}
```

Usandolo:

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

Niente quadri di derisione, niente ginnastica di metodo protetta, niente nomi di metodi basati su stringhe.

## Un esempio reale del mondo: gestore del servizio di traduzione

Ecco un esempio più sofisticato da un gestore di test di servizio di traduzione:

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

Questo responsabile:

- Percorsi diversi percorsi a diversi comportamenti
- Deserializza la richiesta di contenuti per prendere decisioni
- Restituisce codici di errore appropriati per scenari specifici
- È completamente leggibile e auto-documentante

## Impostazione con iniezione di dipendenza

Quando si usa `IHttpClientFactory` (che si dovrebbe essere), l'integrazione dei gestori di test è semplice:

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

Poi nei vostri test:

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

## Modello avanzato: Handler configurabili

Per una maggiore flessibilità, è possibile creare gestori che accettano la configurazione:

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

Uso:

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

## Perché scegliere DelegatingHandler Over Mocks?

| Aspect | Moq-based Mocking | DelegatingHandler |
|--------|-------------------|-------------------|
| **Linee di codice** | Molti | Pochi |
| **Leggibilità** | Basso (cermonia pesante) | Alto (solo C#) |
| **Riutilizzabilità** | Poor | Eccellente |
| **Debug** | Più duro (magia del mock) | Facile (passo attraverso) |
| **Rifattorizzazione** |Brindle |Robusto |
| **Curva di apprendimento** | Ruota (API Moq) | Minimo |
| **Dipendenze** | Richiede Moq | Nessuno (integrato) |

## Quando il mocking ancora fa il senso

Ad essere onesti, ci sono scenari in cui il moq-stile potrebbe ancora essere appropriato:

1. **Risposte semplici una tantum** - Se hai bisogno di un handler a risposta singola una volta, Moq in linea potrebbe essere più veloce
2. **Verifica** - Moq. `Verify()` è utile per affermare che le chiamate sono state fatte
3. **Base di codice esistente** - Se il tuo team ha già un'ampia infrastruttura Moq

Per la verifica, puoi aggiungerlo anche a DelegatingHandler:

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

## Conclusione

Uso `DelegatingHandler` per il test HttpClient ti dà:

- **Codice compatto** - Nessuna cerimonia quadro deridente
- **Prove leggibili** - Solo classi normali di C#
- **Gestori riutilizzabili** - Condividere le classi di test
- **Facile debug** - Impostare i punti di interruzione, passare attraverso il codice
- **Dipendenze zero** - E' integrato in .NET.

La prossima volta che si raggiunge per `Mock<HttpMessageHandler>`, considerare se un semplice `DelegatingHandler` Il tuo futuro te stesso (e i tuoi compagni di squadra) ti ringrazieranno per il codice di prova più pulito e manutenibile.

Vedere i progetti di test in questa soluzione per esempi reali di questo modello in azione.