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
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.
Ecco il tipico HttpMessageHandler scherzo sembra come con 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);
Questo ha diverse questioni:
Protected() e ItExpr perché SendAsync è protettoDelegatingHandler è 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:
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:
var client = new HttpClient(new EchoHandler());
Niente quadri di derisione, niente ginnastica di metodo protetta, niente nomi di metodi basati su stringhe.
Ecco un esempio più sofisticato da un gestore di test di servizio di traduzione:
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:
Quando si usa IHttpClientFactory (che si dovrebbe essere), l'integrazione dei gestori di test è semplice:
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:
[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"));
}
Per una maggiore flessibilità, è possibile creare gestori che accettano la configurazione:
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);
| 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) |
Ad essere onesti, ci sono scenari in cui il moq-stile potrebbe ancora essere appropriato:
Verify() è utile per affermare che le chiamate sono state fattePer la verifica, puoi aggiungerlo anche a DelegatingHandler:
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 per il test HttpClient ti dà:
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.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.