XUnit中Mock HttpMessageHandler未返回响应消息的问题排查
模拟HttpClient匹配特定请求触发System.InvalidOperationException错误
我用Moq为服务编写单元测试,通过Mock HttpMessageHandler模拟HttpClient,但使用config 2或3匹配特定请求时,会抛出错误:System.InvalidOperationException : Handler did not return a response message。只有config 1(匹配任意请求)能正常运行,但我需要精准匹配特定请求。
服务方法代码
public async Task<PaymentDataResponse> PaymentRequest(double amount, string mobileNo) { var client = GetClient(mobileNo); var request = new PaymentRequest() { Amount = amount, CellNumber = SuitablePhoneNumber(mobileNo), TerminalId = _paymentInfoSetting.TerminalId, RedirectUrl = _paymentInfoSetting.RedirectUrl }; var response = await client.PostAsJsonAsync(_paymentInfoSetting.TokenPath, request, new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); if (!response.IsSuccessStatusCode) { throw new PaymentException(); } var responseModel = await response.Content.ReadFromJsonAsync<PaymentResponse>(); return responseModel.Data; } private HttpClient GetClient(string phoneNumber) { var client = _httpClientFactory.CreateClient(); client.BaseAddress = new Uri(_paymentInfoSetting.BaseUrl); client.DefaultRequestHeaders.Add("apiKey", _paymentInfoSetting.ApiKey); client.DefaultRequestHeaders.Add("mobile", phoneNumber); return client; }
单元测试代码
[Fact] public async Task PaymentRequest_ShouldReturnCorrectData() { // Arrange var amount = _fixture.Create<double>(); var mobileNo = _paymentInfoSetting.PhoneNumber; var paymentRequest = _fixture.Build<PaymentRequest>() .With(q => q.Amount, amount).With(q => q.TerminalId, _paymentInfoSetting.TerminalId).With(q => q.RedirectUrl, _paymentInfoSetting.RedirectUrl) .With(q => q.CellNumber, mobileNo) .Create(); var paymentDataResponse = _fixture.Create<PaymentDataResponse>(); var paymentResponse = _fixture.Build<PaymentResponse>().With(q => q.Data, paymentDataResponse).Create(); var response = new HttpResponseMessage(HttpStatusCode.OK); // response.Content = new StringContent(JsonConvert.SerializeObject(paymentResponse), Encoding.UTF8, "application/json"); response.Content = JsonContent.Create(paymentRequest, options: new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var handler = new Mock<HttpMessageHandler>(); // config 1 //handler.Protected() // .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) // .ReturnsAsync(response); // config 2 //handler.Protected() // .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(req => // req.Content.ReadAsStringAsync().Result == JsonConvert.SerializeObject(paymentRequest)), ItExpr.IsAny<CancellationToken>()) // .ReturnsAsync(response); // config 3 handler.Protected() .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(req => req.Content == JsonContent.Create(paymentRequest, null, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) ), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(response); var httpClient = new HttpClient(handler.Object); _httpClientFactoryMock.Setup(q => q.CreateClient(It.IsAny<string>())).Returns(httpClient); // Act var result = await _service.PaymentRequest(amount, mobileNo); var resultJson = JsonConvert.SerializeObject(result); var paymentDataResponseJson = JsonConvert.SerializeObject(paymentDataResponse); // Assert _httpClientFactoryMock.Verify(q => q.CreateClient(It.IsAny<string>()), Times.Once); resultJson.ShouldBe(paymentDataResponseJson); }
问题原因分析
- config 3匹配失败:每次调用
JsonContent.Create都会生成全新的对象实例,用==比较引用地址肯定不相等,导致Mock的SendAsync从未被触发,Handler没有返回响应。 - config 2匹配失败:
- 同步调用
ReadAsStringAsync().Result容易引发死锁,而且测试用Newtonsoft.Json序列化,服务用System.Text.Json(还指定了CamelCase命名策略),两者序列化结果可能不一致; - 服务里调用了
SuitablePhoneNumber(mobileNo)处理手机号,但测试里的paymentRequest直接用了原始手机号,若该方法修改了手机号(比如去除空格、添加区号),请求中的CellNumber和测试预期值不匹配。
- 同步调用
- 额外错误:响应内容设置错误——服务期望返回
PaymentResponse,但当前代码把paymentRequest作为响应内容,会导致服务中ReadFromJsonAsync<PaymentResponse>失败。
解决方案
第一步:修正响应内容
先把响应内容改成正确的PaymentResponse:
response.Content = JsonContent.Create(paymentResponse, options: new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
第二步:正确匹配请求
改用异步方式读取请求内容并反序列化,比较关键属性,同时处理手机号的一致性:
handler.Protected() .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(async req => { var requestContent = await req.Content.ReadFromJsonAsync<PaymentRequest>(); // 模拟或调用服务中的手机号处理逻辑,确保和服务生成的一致 var processedMobile = SuitablePhoneNumber(mobileNo); return requestContent.Amount == amount && requestContent.CellNumber == processedMobile && requestContent.TerminalId == _paymentInfoSetting.TerminalId && requestContent.RedirectUrl == _paymentInfoSetting.RedirectUrl; }), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(response);
如果SuitablePhoneNumber是服务的私有方法,可以把它提取为公共方法,或者在测试中预先处理手机号,确保和服务生成的CellNumber一致。
关键注意点
- 不要用
==比较JsonContent对象,每次创建都是新实例,引用不相等; - 避免用
.Result同步调用异步方法,Moq支持在匹配条件中使用异步lambda; - 确保测试中的请求参数和服务实际生成的参数一致(比如经过处理的手机号);
- 响应内容要和服务期望的返回类型匹配。
内容的提问来源于stack exchange,提问作者Soheil
相关产品推荐
相关产品推荐

