You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从Moq迁移至NSubstitute:Mock HttpMessageHandler受保护方法方案

迁移Moq到NSubstitute:Mock HttpMessageHandler受保护方法的解决方案

NSubstitute确实没有像Moq那样直接通过Protected()API设置受保护方法表达式匹配的功能,但可以通过自定义HttpMessageHandler子类实现完全等价的逻辑,同时保留你原有的测试调用风格。

实现步骤

1. 自定义可配置的HttpMessageHandler

这个类负责存储请求匹配规则、响应生成逻辑,以及记录请求用于后续验证:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Moq; // 若不想依赖Moq,可自行实现Times类

public class MockableHttpMessageHandler : HttpMessageHandler
{
    private readonly List<(Expression<Func<HttpRequestMessage, bool>> Match, Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> ResponseFactory)> _responseRules = new();
    private readonly List<(Expression<Func<HttpRequestMessage, bool>> Match, Times ExpectedTimes)> _verificationRules = new();
    private readonly List<HttpRequestMessage> _capturedRequests = new();

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _capturedRequests.Add(request);
        var matchingRule = _responseRules.FirstOrDefault(r => r.Match.Compile()(request));
        return matchingRule.ResponseFactory != null 
            ? await matchingRule.ResponseFactory(request, cancellationToken) 
            : new HttpResponseMessage(System.Net.HttpStatusCode.NotFound);
    }

    public void AddResponseRule(Expression<Func<HttpRequestMessage, bool>> match, Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseFactory)
    {
        _responseRules.Add((match, responseFactory));
    }

    public void AddVerificationRule(Expression<Func<HttpRequestMessage, bool>> match, Times expectedTimes)
    {
        _verificationRules.Add((match, expectedTimes));
    }

    public void VerifyAll()
    {
        foreach (var rule in _verificationRules)
        {
            var matchFunc = rule.Match.Compile();
            var callCount = _capturedRequests.Count(matchFunc);
            var expected = rule.ExpectedTimes;

            if (expected is Times.Exactly exactly)
            {
                if (callCount != exactly.Count)
                    throw new AssertionException($"Request matching expression was called {callCount} times, expected {exactly.Count}.");
            }
            else if (expected == Times.Once() && callCount != 1)
                throw new AssertionException($"Request matching expression was called {callCount} times, expected once.");
            else if (expected == Times.Never() && callCount > 0)
                throw new AssertionException($"Request matching expression was called {callCount} times, expected never.");
            // 按需扩展AtLeast/AtMost等匹配逻辑
        }
    }
}

2. 封装原有的Setup/Verify方法

替换原来基于Moq的方法,改为使用自定义Handler,保持测试代码的调用方式不变:

using Newtonsoft.Json;
using System.Net;
using System.Net.Http;
using System.Text;

// 在测试基类中定义
protected MockableHttpMessageHandler HttpMessageHandler { get; } = new MockableHttpMessageHandler();

protected void SetupHandler<T>(Expression<Func<HttpRequestMessage, bool>> match, HttpStatusCode httpStatusCode, T body)
{
    SetupHandlerStringResponse(match, httpStatusCode, JsonConvert.SerializeObject(body), "application/json");
}

protected void SetupHandlerStringResponse(Expression<Func<HttpRequestMessage, bool>> match, HttpStatusCode httpStatusCode, string body, string contentType)
{
    HttpMessageHandler.AddResponseRule(match, (req, ct) => Task.FromResult(new HttpResponseMessage
    {
        StatusCode = httpStatusCode,
        Content = new StringContent(body, Encoding.UTF8, contentType)
    }));
}

protected void VerifyHandler(Expression<Func<HttpRequestMessage, bool>> match, Times times)
{
    HttpMessageHandler.AddVerificationRule(match, times);
}

// 在测试清理阶段执行验证
[TearDown]
public void Cleanup()
{
    HttpMessageHandler.VerifyAll();
}

3. 测试代码完全复用原有逻辑

调用方式和之前使用Moq时完全一致,不需要修改测试用例:

SetupHandler(t => t.Method == HttpMethod.Get, HttpStatusCode.OK, lookup);

var response = await Class.DoSomething();

Assert.NotNull(response);

VerifyHandler(t => t.Method == HttpMethod.Get && t.RequestUri.PathAndQuery == $"/api/endpointName", Times.Once());

补充说明

  • 如果不想依赖Moq的Times类,可以自行实现一个简化版本,比如包含Once()、Never()、Exactly(int)等静态方法。
  • 自定义Handler的方式比依赖NSubstitute的protected方法扩展更直观,也更容易调试和扩展额外逻辑(比如捕获请求内容、添加延迟等)。

内容的提问来源于stack exchange,提问作者Alexander Hardy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 21:52:46