# 无鼠标 HttpClient 单位测试

<datetime class="hidden">2025-11-29T07:00</datetime>

<!--category-- xUnit, Unit Testing, HttpClient -->
## 一. 导言导言 导言 导言 导言

当测试使用代码时 `HttpClient`传统方法包括嘲弄。 `HttpMessageHandler` 使用像 Moq.+#这样的框架, 虽然这有用 @ ,+} 它可以是verbose @ MPK2_ 仪式@ MS K3 hheavy,+# 坦率地说有点丑陋\ .Q there{'Qs a clean option@:_ 使用 `DelegatingHandler` 创建测试处理器, 使其行为像真正的 HTTP 端点@ . @ {}

这篇文章I'将展示你为何会完全跳过嘲笑, `DelegatingHandler` 和紧凑测试代码@.}

[TOC]

## 模拟 HttpMessageHandler 的问题

这里#'}这是典型的 `HttpMessageHandler` 与 Moq: 一样,

```csharp
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`:

```csharp
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")
        };
    }
}
```

使用它#:

```csharp
var client = new HttpClient(new EchoHandler());
```

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}没有被保护的体操方法?

## 翻译服务处理器

翻譯服務檢測員@: @%

```csharp
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` (}您应该成为的),}整合测试处理器直截了当 *:}

```csharp
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;
}
```

然后在你的测试中 :

```csharp
[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

```csharp
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);
    }
}
```

用法@: @%

```csharp
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:}

```csharp
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_.}请查看“ 行动” 中这种模式的例子。