当测试使用代码时 HttpClient传统方法涉及嘲弄 HttpMessageHandler 使用像 Moq 这样的框架。 虽然这有效, 但它可以是动词, 仪式重, 坦率地说, 有点丑陋。 还有一个更干净的替代方案: 使用 。 DelegatingHandler 创建测试处理器,使其行为像真正的 HTTP 端点 。
在这个职位上,我要向大家说明,为什么你们会完全跳过模拟,完全使用 DelegatingHandler 用于更可读、可维持和紧凑的测试代码。
这就是典型的 HttpMessageHandler 嘲笑像莫克人:
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);
这有几个问题:
Protected() 和 ItExpr 原因原因 SendAsync 受保护DelegatingHandler 是一个内置的. NET 类, 设计就是为了这个目的 在HTTP 请求进入网络之前截取它们的请求。 这是在生产中使用的中间软件, 比如重试处理器、 伐木处理器和认证处理器。
这里的功能相同 使用 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")
};
}
}
使用:
var client = new HttpClient(new EchoHandler());
就是这样,没有模拟框架,没有受保护的方法体操,没有基于字符串的方法名称。
以下是翻译服务测试处理器更精密的例子:
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)
};
}
}
此处理器 :
使用时使用 IHttpClientFactory 将试验处理器整合起来是简单明了的:
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;
}
然后在你的测试中:
[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"));
}
为了更具灵活性,您可以创建能够接受配置的处理器:
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);
}
}
用法 :
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);
以摩克為基礎的嘲笑 提名漢德勒
| -------- | ------------------- | ------------------- |
|---|---|---|
| 可易读性 低(礼仪重) 高(仅C#) | ||
| 可再性 可怜的 棒极了 | ||
| 除调调 用力点(模仿魔术) 轻点(一步步通过) | ||
| 重构 | ||
| 学习曲线 斯迪普(Moq APIs) Minimal | ||
| 依赖性 需要Moq (内建) |
公平而言,Moq式的嘲笑可能仍然适当:
Verify() 发出呼吁是有用的用于核查,您也可以将其添加到Handler代表处:
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));
}
}
使用 DelegatingHandler HttpClient 测试给您提供 :
下次你再伸手找 Mock<HttpMessageHandler>,考虑是否简单的 DelegatingHandler 你的未来自己(和你的队友)会感谢你们 提供了更干净、更适合维护的测试代码
有关这种模式的实际实例,请见这一解决办法中的试验项目。
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.