无鼠标 HttpClient 单位测试 (中文 (Chinese Simplified))

无鼠标 HttpClient 单位测试

Saturday, 29 November 2025

//

4 minute read

一. 导言导言 导言 导言 导言

当测试使用代码时 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);

这有几个问题 :

  1. 柔软 @- 许多锅炉板 来做简单的行为
  2. 保护方法仪式 "你需要" Protected() 和 ItExpr 因为 SendAsync 受保护
  3. 难读 实际的测试逻辑被埋在设置仪式上
  4. 不可再使用 每一次测试都需要类似的设置代码
  5. 饼干 & - 容易获得基于字符串的 -}方法命名错误

杰出汉人替代方案

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Š- 式的嘲弄可能仍然合适 @: @

  1. 简单回应 - 如果您需要單一的 -回應處理器
  2. 核查 @- @ MoqQ'%s Verify() 发出呼吁是有用的
  3. 现有代码库 如果你的团队已经拥有了庞大的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_.}请查看“ 行动” 中这种模式的例子。

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.