无鼠标 HttpClient 单位测试
一. 导言导言 导言 导言 导言
当测试使用代码时 HttpClient传统方法包括嘲弄。 HttpMessageHandler 使用像 Moq.+#这样的框架, 虽然这有用 @ ,+} 它可以是verbose @ MPK2_ 仪式@ MS K3 hheavy,+# 坦率地说有点丑陋\ .Q there{'Qs a clean option@:_ 使用 DelegatingHandler 创建测试处理器, 使其行为像真正的 HTTP 端点@ . @
这篇文章I'将展示你为何会完全跳过嘲笑, DelegatingHandler 和紧凑测试代码@.}
模拟 HttpMessageHandler 的问题
这里#'}这是典型的 HttpMessageHandler 与 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);
这有几个问题 :
- 柔软 @- 许多锅炉板 来做简单的行为
- 保护方法仪式 "你需要"
Protected()和ItExpr因为SendAsync受保护 - 难读 实际的测试逻辑被埋在设置仪式上
- 不可再使用 每一次测试都需要类似的设置代码
- 饼干 & - 容易获得基于字符串的 -}方法命名错误
杰出汉人替代方案
DelegatingHandler 是一个专门为此设计的“.”NET类 : “-” 在HTTP进入网络前拦截 HTTP 请求@. @ It'}#中继软件,比如重试处理器,}伐木操作器_,}和在生产中使用的认证处理器>.}
这里的'}使用相同的功能 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());
{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}没有被保护的体操方法?
翻译服务处理器
翻譯服務檢測員@: @%
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)
};
}
}
此处理器@ : @
- 选择不同行为的不同途径
- 取消决策请求内容的排序
- 为具体情景返回适当的错误代码
- 完全可读和自定义@- @% document
使用依赖性注射设置
使用时 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"));
}
高级模式@:可配置手持器
您可以创建接受配置的操作器@ : @ { label
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);
为何选择 Moks?
-=YTET -伊甸园字幕组=- 翻译: |--------|-------------------|-------------------| | 代码行 @| @ many @ | @ 少许@| @ | 可易读性 -=YTET -伊甸园字幕组=- 翻译: | 可再性 可怜的|,很棒的| | 调试 @|+Harder@(#mock 魔术=)} @MS K3+Easy #(#通过)| | 重构 |+Brittle+|+强势 | 学习曲线 |+Steeper+(+MOQAPIs=)+|+MISK4}最微小的| | 依赖性 | 需要 Moq @| @ non _(_建立-}){|}
当嘲笑仍然让人发觉时
为了公平起见,, 有一些假想: MoqŠ- 式的嘲弄可能仍然合适 @: @
- 简单回应 - 如果您需要單一的 -回應處理器
- 核查 @- @ MoqQ'%s
Verify()发出呼吁是有用的 - 现有代码库 如果你的团队已经拥有了庞大的Moq基础设施
用于校验@, @% 您可以将其添加到指定 Handler too:}
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 测试的 Http Client 为您提供了@: @%
- 契约守则 - 禁止嘲弄框架仪式
- 可读测试 @- 普通的C#班
- 可再使用处理器 - 跨测试班共享
- 容易调试 - 设置断点 @, 通过代码一步
- 零依赖性 @- @ it'_% 建在@.NET
下次你再伸手找 Mock<HttpMessageHandler>, 考虑是否简单 DelegatingHandler 将感谢你们更清洁的,测试码=.*
请参见此解决方案中的测试项目, 真实的@- @world example of the transaction in action_.}请查看“ 行动” 中这种模式的例子。