# Prueba de unidad HttpClient SIN mocks

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

<!--category-- xUnit, Unit Testing, HttpClient -->
## Introducción

Cuando se prueba el código que utiliza `HttpClient`, el enfoque tradicional implica burlas `HttpMessageHandler` utilizar marcos como Moq. Mientras que esto funciona, puede ser verboso, ceremonia-pesada, y francamente un poco feo. Hay una alternativa más limpia: el uso de `DelegatingHandler` para crear manejadores de prueba que se comporten como endpoints HTTP reales.

En este post te mostraré por qué puedes saltarte las burlas por completo y usar `DelegatingHandler` para un código de prueba más legible, mantenible y compacto.

[TOC]

## El problema de mocking HttpMessageHandler

Esto es lo típico. `HttpMessageHandler` burlarse se parece a 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);
```

Se trata de varias cuestiones:

1. **Verbosa** - Mucha placa de caldera para lo que debería ser un comportamiento sencillo
2. **Ceremonia del método protegido** - Necesitas `Protected()` y `ItExpr` porque `SendAsync` está protegido
3. **Difícil de leer** - La lógica real de la prueba está enterrada en la ceremonia de instalación
4. **No reutilizable** - Cada prueba necesita un código de configuración similar
5. **Brittle** - Fácil de obtener el nombre del método basado en cadenas mal

## La alternativa de delegar a Handler

`DelegatingHandler` es una clase .NET integrada diseñada exactamente para este propósito - interceptando peticiones HTTP antes de que lleguen a la red. Es lo que middleware como los manejadores de reintentar, manejadores de registro y manejadores de autenticación utilizan en la producción.

Aquí está la misma funcionalidad 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")
        };
    }
}
```

Usándolo:

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

Sin marcos de burla, sin gimnasia de métodos protegidos, sin nombres de métodos basados en cuerdas.

## Un ejemplo en el mundo real: Manejador de servicios de traducción

He aquí un ejemplo más sofisticado de un controlador de pruebas de servicio de traducción:

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

Este manejador:

- Rutas diferentes caminos a diferentes comportamientos
- Deserializa el contenido de la solicitud para tomar decisiones
- Devuelve los códigos de error apropiados para escenarios específicos
- Es completamente legible y autodocumentativo

## Configuración con inyección de dependencia

Cuando se utiliza `IHttpClientFactory` (lo que debería ser), integrar los manejadores de pruebas es sencillo:

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

Luego, en sus pruebas:

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

## Patrón avanzado: Manipuladores configurables

Para mayor flexibilidad, puede crear manejadores que acepten la configuración:

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

## ¿Por qué elegir delegando a Handler sobre mocks?

Aspecto Moq-basado en mocking DelegatingHandler
|--------|-------------------|-------------------|
| **Líneas de código** # Muchos # Pocos #
| **Legibilidad** Baja (elevada ceremonia) Alta (sólo C#)
| **Reutilizabilidad** # Pobre # # Excelente #
| **Depuración** Más duro (magia de mock) Fácil (paso a través)
| **Refactorización** # Brittle # # Robusto #
| **Curva de aprendizaje** Steeper (API de Moq)  Mínimo
| **Dependencias** Requiere Moq  Ninguno (construido)

## Cuando el mocoso todavía tiene sentido

Para ser justos, hay escenarios en los que las burlas al estilo Moq podrían ser apropiadas:

1. **Respuestas sencillas puntuales** - Si necesita un controlador de una sola respuesta una vez, el Moq en línea podría ser más rápido
2. **Verificación** - Moq's `Verify()` es útil para afirmar que se hicieron llamadas
3. **Base de códigos existente** - Si su equipo ya tiene una amplia infraestructura Moq

Para la verificación, puede añadirlo a DelegatingHandler también:

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

## Conclusión

Uso `DelegatingHandler` para la prueba de HttpClient le da:

- **Código compacto** - No hay ceremonia marco de burla
- **Pruebas legibles** - Sólo clases normales de C#.
- **Manipuladores reutilizables** - Compartir entre las clases de prueba
- **Depuración fácil** - Establecer puntos de interrupción, paso a través del código
- **Cero dependencias** - Está integrado en .NET.

La próxima vez que busques `Mock<HttpMessageHandler>`, considerar si un simple `DelegatingHandler` Su futuro yo (y sus compañeros de equipo) le agradecerán por el código de prueba más limpio y más mantenible.

Vea los proyectos de prueba en esta solución para ver ejemplos reales de este patrón en acción.