导语
在 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<string>())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);
}
}
}