This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Saturday, 29 November 2025
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.
Esto es lo típico. HttpMessageHandler burlarse se parece a Moq:
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:
Protected() y ItExpr porque SendAsync está protegidoDelegatingHandler 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:
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:
var client = new HttpClient(new EchoHandler());
Sin marcos de burla, sin gimnasia de métodos protegidos, sin nombres de métodos basados en cuerdas.
He aquí un ejemplo más sofisticado de un controlador de pruebas de servicio de traducción:
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:
Cuando se utiliza IHttpClientFactory (lo que debería ser), integrar los manejadores de pruebas es sencillo:
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:
[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"));
}
Para mayor flexibilidad, puede crear manejadores que acepten la configuración:
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:
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);
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)
Para ser justos, hay escenarios en los que las burlas al estilo Moq podrían ser apropiadas:
Verify() es útil para afirmar que se hicieron llamadasPara la verificación, puede añadirlo a DelegatingHandler también:
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));
}
}
Uso DelegatingHandler para la prueba de HttpClient le da:
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.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.