Testattaessa koodia, joka käyttää HttpClientPerinteiseen lähestymistapaan kuuluu pilkkaaminen HttpMessageHandler Moqin kaltaisia kehyksiä käyttäen. Vaikka tämä toimii, se voi olla sanavalmis, seremoniapainotteinen ja suoraan sanottuna hieman ruma. On olemassa puhtaampi vaihtoehto: käyttämällä DelegatingHandler Luoda testikäsittelijöitä, jotka käyttäytyvät todellisten HTTP-päätteiden mukaisesti.
Tässä viestissä näytän, miksi voit jättää mokat kokonaan väliin ja käyttää DelegatingHandler Lisää luettavaa, ylläpidettävää ja kompaktia testikoodia.
Tämä on tyypillistä HttpMessageHandler pilkkaaminen näyttää Moqin kanssa:
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);
Tässä on useita ongelmia:
Protected() sekä ItExpr koska SendAsync on suojattuDelegatingHandler Se on sisäänrakennettu .NET-luokka, joka on suunniteltu juuri tähän tarkoitukseen - siepaten HTTP-pyynnöt ennen kuin ne osuvat verkkoon. Sitä keskiohjelmistot, kuten uudelleenryöstäjät, puunkäsittelyn käsittelijät ja todentamisen käsittelijät, käyttävät tuotannossa.
Tässä on sama toiminnallisuus 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")
};
}
}
Käyttämällä sitä:
var client = new HttpClient(new EchoHandler());
Ei pilkkakehyksiä, ei suojattua metodin voimistelua, ei narupohjaisia nimiä.
Tässä on hienostuneempi esimerkki käännöspalvelun testikäsittelijältä:
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)
};
}
}
Tämä käsittelijä:
Käytettäessä IHttpClientFactory Testien käsittelijöiden integroiminen on yksinkertaista:
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;
}
Sitten testeissäsi:
[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"));
}
Joustavuuden lisäämiseksi voit luoda käsittelijöitä, jotka hyväksyvät kokoonpanon:
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);
}
}
Käyttö:
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);
Moq-pohjainen Mocking-delegointiHandler-delegointi |--------|-------------------|-------------------| | Koodirivit Monia harvalukuisia | Luettavuus Alhainen (seremonia raskas) Korkea (vain C#) | Uudelleenkäytettävyys Köyhät loistavat | Vianetsintä Kovempaa (mock magic) Helppoa (step through) | Uusintakerroin "Lahjus ryöppyää" | Oppimiskäyrä Steeper (Moqin rajapinnat) Minimaaliset rajapinnat | Riippuvuussuhteet Vaatii Moq None (sisäänrakennettu)
Ollakseni reilu, on olemassa skenaarioita, joissa moq-tyylinen pilkkaaminen voisi edelleen olla tarkoituksenmukaista:
Verify() On hyödyllistä vakuuttaa puheluita soitettiinVarmennusta varten voit lisätä sen myös DelegatingHandleriin:
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));
}
}
Käyttäminen DelegatingHandler HttpClient-testauksesta saat seuraavat tiedot:
Seuraavalla kerralla kurotat Mock<HttpMessageHandler>, harkitse, onko yksinkertainen DelegatingHandler Tuleva minäsi (ja joukkuetoverisi) kiittää sinua puhtaammasta, huollettavammasta testikoodista.
Katso tämän ratkaisun testihankkeista tosimaailman esimerkkejä tästä kuviosta käytännössä.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.