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

2021-05-27 14:38:01 浏览数 (1)

导语

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

}

}

}

0 人点赞