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
Bij het testen van code die gebruik maakt van HttpClient, de traditionele aanpak houdt bespotting in HttpMessageHandler met behulp van kaders zoals Moq. Terwijl dit werkt, kan het werkbose, ceremonie-zware, en eerlijk gezegd een beetje lelijk. Er is een schoner alternatief: gebruik DelegatingHandler om testverwerkers te maken die zich gedragen als echte HTTP-eindpunten.
In dit bericht zal ik laten zien waarom je zou kunnen overslaan de mocks volledig en gebruik DelegatingHandler voor meer leesbaar, onderhoudbaar en compacte testcode.
Dit is wat typisch is. HttpMessageHandler spotten lijkt op met 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);
Dit heeft verschillende kwesties:
Protected() en ItExpr omdat SendAsync is beschermdDelegatingHandler is een ingebouwde .NET klasse ontworpen voor precies dit doel - het onderscheppen van HTTP-verzoeken voordat ze het netwerk raken. Het is wat middleware zoals retry handlers, logging handlers, en authenticatie handlers gebruiken in de productie.
Hier is dezelfde functionaliteit met behulp van 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")
};
}
}
Met behulp van het:
var client = new HttpClient(new EchoHandler());
Geen bespotting kaders, geen beschermde methode gymnastiek, geen op string gebaseerde methode namen.
Hier is een meer verfijnd voorbeeld van een vertaaldienst test handler:
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)
};
}
}
Deze begeleider:
Bij gebruik IHttpClientFactory (wat je zou moeten zijn), het integreren van testverwerkers is eenvoudig:
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;
}
Dan in uw tests:
[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"));
}
Voor meer flexibiliteit kunt u handlers creëren die configuratie accepteren:
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);
}
}
Gebruik:
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);
Mocking op basis van Moq DelegingHandler |--------|-------------------|-------------------| | Coderegels Veel weinig | Leesbaarheid Laag (ceremonie zwaar) Hoog (gewoon C#) | Herbruikbaarheid Uitstekend. | Debuggen Harder (mock magie) . . Eenvoudig (stap door) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . | Refactoring Brettle Robuust | Leercurve Steeper (Moq API's) | Afhankelijkheden Moq vereist geen (ingebouwd)
Om eerlijk te zijn, zijn er scenario's waarin Moq-stijl spotten nog steeds geschikt zou kunnen zijn:
Verify() is nuttig voor het bevestigen van oproepen werden gedaanVoor verificatie, kunt u het toevoegen aan VerwijderingHandler ook:
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));
}
}
Gebruik DelegatingHandler voor HttpClient testen geeft u:
De volgende keer dat je Mock<HttpMessageHandler>, na te gaan of een eenvoudige DelegatingHandler Je toekomstige zelf (en je teamgenoten) zal je dankbaar zijn voor de schonere, duurzamere testcode.
Zie de testprojecten in deze oplossing voor real-world voorbeelden van dit patroon in actie.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.