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

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

问题原因分析

  1. config 3匹配失败:每次调用JsonContent.Create都会生成全新的对象实例,用==比较引用地址肯定不相等,导致Mock的SendAsync从未被触发,Handler没有返回响应。
  2. config 2匹配失败:
    • 同步调用ReadAsStringAsync().Result容易引发死锁,而且测试用Newtonsoft.Json序列化,服务用System.Text.Json(还指定了CamelCase命名策略),两者序列化结果可能不一致;
    • 服务里调用了SuitablePhoneNumber(mobileNo)处理手机号,但测试里的paymentRequest直接用了原始手机号,若该方法修改了手机号(比如去除空格、添加区号),请求中的CellNumber和测试预期值不匹配。
  3. 额外错误:响应内容设置错误——服务期望返回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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 15:33:14