从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
相关产品推荐
相关产品推荐

