本文转载自微信公众号「汪宇杰博客」,作者汪宇杰。转载本文请联系汪宇杰博客公众号。
在 ASP.NET Core 单元测试中模拟HttpClient.GetStringAsync() 的技巧。
问题
下面这个代码
- var html = await _httpClient.GetStringAsync(sourceUrl);
如果按正常思路像这样去 Mock HttpClient.GetStringAsync()
- var httpClientMock = new Mock<HttpClient>();
- httpClientMock
- .Setup(p => p.GetStringAsync(It.IsAny<string>()))
- .Returns(Task.FromResult("..."));
Moq 框架就会爆
Exception
- 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
- var handlerMock = new Mock<HttpMessageHandler>();
- var magicHttpClient = new HttpClient(handlerMock.Object);
然后我花了 9.96 分钟研究了 HttpClient.GetStringAsync() 的源代码,发现它最终调用的是 SendAsync() 方法
- private async Task<string> GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- // ...
- response = await base.SendAsync(request, cts.Token).ConfigureAwait(false);
- // ...
- }
源代码位置:https://source.dot.net/#System.Net.Http/System/Net/Http/HttpClient.cs,170
因此,我们的 Mock Setup 如下:
- handlerMock
- .Protected()
- .Setup<Task<HttpResponseMessage>>(
- "SendAsync",
- ItExpr.IsAny<HttpRequestMessage>(),
- ItExpr.IsAny<CancellationToken>()
- )
- .ReturnsAsync(new HttpResponseMessage
- {
- StatusCode = HttpStatusCode.OK,
- Content = new StringContent("the string you want to return")
- })
- .Verifiable();
现在 Mock 就能运行成功了!
最后附上完整的 UT 代码供参考:
- using System.Net;
- using System.Net.Http;
- using System.Threading;
- using System.Threading.Tasks;
- using Microsoft.Extensions.Logging;
- using Moq;
- using Moq.Protected;
- using NUnit.Framework;
- namespace Moonglade.Pingback.Tests
- {
- [TestFixture]
- public class PingSourceInspectorTests
- {
- private MockRepository _mockRepository;
- private Mock<ILogger<PingSourceInspector>> _mockLogger;
- private Mock<HttpMessageHandler> _handlerMock;
- private HttpClient _magicHttpClient;
- [SetUp]
- public void SetUp()
- {
- _mockRepository = new(MockBehavior.Default);
- _mockLogger = _mockRepository.Create<ILogger<PingSourceInspector>>();
- _handlerMock = _mockRepository.Create<HttpMessageHandler>();
- }
- private PingSourceInspector CreatePingSourceInspector()
- {
- _magicHttpClient = new(_handlerMock.Object);
- return new(_mockLogger.Object, _magicHttpClient);
- }
- [Test]
- public async Task ExamineSourceAsync_StateUnderTest_ExpectedBehavior()
- {
- string sourceUrl = "https://996.icu/work-996-sick-icu";
- string targetUrl = "https://greenhat.today/programmers-special-gift";
- _handlerMock
- .Protected()
- .Setup<Task<HttpResponseMessage>>(
- "SendAsync",
- ItExpr.IsAny<HttpRequestMessage>(),
- ItExpr.IsAny<CancellationToken>()
- )
- .ReturnsAsync(new HttpResponseMessage
- {
- StatusCode = HttpStatusCode.OK,
- Content = new StringContent($"<html>" +
- $"<head>" +
- $"<title>Programmer's Gift</title>" +
- $"</head>" +
- $"<body>Work 996 and have a <a href=\"{targetUrl}\">green hat</a>!</body>" +
- $"</html>")
- })
- .Verifiable();
- var pingSourceInspector = CreatePingSourceInspector();
- var result = await pingSourceInspector.ExamineSourceAsync(sourceUrl, targetUrl);
- Assert.IsFalse(result.ContainsHtml);
- Assert.IsTrue(result.SourceHasLink);
- Assert.AreEqual("Programmer's Gift", result.Title);
- Assert.AreEqual(targetUrl, result.TargetUrl);
- Assert.AreEqual(sourceUrl, result.SourceUrl);
- }
- }
- }