ASP.NET Core 单元测试之如何 Mock HttpClient.GetStringAsync()

时间:2022-09-10 09:23:34

 ASP.NET Core 单元测试之如何 Mock HttpClient.GetStringAsync()

本文转载自微信公众号「汪宇杰博客」,作者汪宇杰。转载本文请联系汪宇杰博客公众号。

在 ASP.NET Core 单元测试中模拟HttpClient.GetStringAsync() 的技巧。

问题

 

下面这个代码

  1. var html = await _httpClient.GetStringAsync(sourceUrl); 

如果按正常思路像这样去 Mock HttpClient.GetStringAsync()

  1. var httpClientMock = new Mock<HttpClient>(); 
  2. httpClientMock 
  3.     .Setup(p => p.GetStringAsync(It.IsAny<string>())) 
  4.     .Returns(Task.FromResult("...")); 

Moq 框架就会爆

 

Exception

  1. System.NotSupportedException : Unsupported expression: p => p.GetStringAsync(It.IsAny())Non-overridable members (here: HttpClient.GetStringAsync) may not be used in setup / verification expressions. 

解决方法

 

我们需要 Mock HttpClient 底层使用的 HttpMessageHandler 而不是 HttpClient

  1. var handlerMock = new Mock<HttpMessageHandler>(); 
  2. var magicHttpClient = new HttpClient(handlerMock.Object); 

然后我花了 9.96 分钟研究了 HttpClient.GetStringAsync() 的源代码,发现它最终调用的是 SendAsync() 方法

  1. private async Task<string> GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken) 
  2.     // ... 
  3.     response = await base.SendAsync(request, cts.Token).ConfigureAwait(false); 
  4.     // ... 

源代码位置:https://source.dot.net/#System.Net.Http/System/Net/Http/HttpClient.cs,170

因此,我们的 Mock Setup 如下:

  1. handlerMock 
  2.     .Protected() 
  3.     .Setup<Task<HttpResponseMessage>>( 
  4.         "SendAsync"
  5.         ItExpr.IsAny<HttpRequestMessage>(), 
  6.         ItExpr.IsAny<CancellationToken>() 
  7.     ) 
  8.     .ReturnsAsync(new HttpResponseMessage 
  9.     { 
  10.         StatusCode = HttpStatusCode.OK, 
  11.         Content = new StringContent("the string you want to return"
  12.     }) 
  13.     .Verifiable(); 

现在 Mock 就能运行成功了!

最后附上完整的 UT 代码供参考:

  1. using System.Net; 
  2. using System.Net.Http; 
  3. using System.Threading; 
  4. using System.Threading.Tasks; 
  5. using Microsoft.Extensions.Logging; 
  6. using Moq; 
  7. using Moq.Protected; 
  8. using NUnit.Framework; 
  9.  
  10. namespace Moonglade.Pingback.Tests 
  11.     [TestFixture] 
  12.     public class PingSourceInspectorTests 
  13.     { 
  14.         private MockRepository _mockRepository; 
  15.  
  16.         private Mock<ILogger<PingSourceInspector>> _mockLogger; 
  17.         private Mock<HttpMessageHandler> _handlerMock; 
  18.         private HttpClient _magicHttpClient; 
  19.  
  20.         [SetUp] 
  21.         public void SetUp() 
  22.         { 
  23.             _mockRepository = new(MockBehavior.Default); 
  24.             _mockLogger = _mockRepository.Create<ILogger<PingSourceInspector>>(); 
  25.             _handlerMock = _mockRepository.Create<HttpMessageHandler>(); 
  26.         } 
  27.  
  28.         private PingSourceInspector CreatePingSourceInspector() 
  29.         { 
  30.             _magicHttpClient = new(_handlerMock.Object); 
  31.             return new(_mockLogger.Object, _magicHttpClient); 
  32.         } 
  33.  
  34.         [Test] 
  35.         public async Task ExamineSourceAsync_StateUnderTest_ExpectedBehavior() 
  36.         { 
  37.             string sourceUrl = "https://996.icu/work-996-sick-icu"
  38.             string targetUrl = "https://greenhat.today/programmers-special-gift"
  39.  
  40.             _handlerMock 
  41.                 .Protected() 
  42.                 .Setup<Task<HttpResponseMessage>>( 
  43.                     "SendAsync"
  44.                     ItExpr.IsAny<HttpRequestMessage>(), 
  45.                     ItExpr.IsAny<CancellationToken>() 
  46.                 ) 
  47.                 .ReturnsAsync(new HttpResponseMessage 
  48.                 { 
  49.                     StatusCode = HttpStatusCode.OK, 
  50.                     Content = new StringContent($"<html>" + 
  51.                                                 $"<head>" + 
  52.                                                 $"<title>Programmer's Gift</title>" + 
  53.                                                 $"</head>" + 
  54.                                                 $"<body>Work 996 and have a <a href=\"{targetUrl}\">green hat</a>!</body>" + 
  55.                                                 $"</html>"
  56.                 }) 
  57.                 .Verifiable(); 
  58.             var pingSourceInspector = CreatePingSourceInspector(); 
  59.  
  60.             var result = await pingSourceInspector.ExamineSourceAsync(sourceUrl, targetUrl); 
  61.             Assert.IsFalse(result.ContainsHtml); 
  62.             Assert.IsTrue(result.SourceHasLink); 
  63.             Assert.AreEqual("Programmer's Gift", result.Title); 
  64.             Assert.AreEqual(targetUrl, result.TargetUrl); 
  65.             Assert.AreEqual(sourceUrl, result.SourceUrl); 
  66.         } 
  67.     } 

原文链接:https://mp.weixin.qq.com/s/Ljst8xxnC0iURU4Ev6RSpQ