In ASP.NET Core unit tests, if you want to mock HttpClient.GetStringAsync()
, here's the trick.
Problem
Given the following code
var html = await _httpClient.GetStringAsync(sourceUrl);
When I tried to mock HttpClient.GetStringAsync()
like this
var httpClientMock = new Mock<HttpClient>();
httpClientMock
.Setup(p => p.GetStringAsync(It.IsAny<string>()))
.Returns(Task.FromResult("..."));
Moq will blow up sky high
System.NotSupportedException : Unsupported expression: p => p.GetStringAsync(It.IsAny<string>())
Non-overridable members (here: HttpClient.GetStringAsync) may not be used in setup / verification expressions.
Solution
Instead of mocking HttpClient
type, we need to mock the underlying HttpMessageHandler
that HttpClient
uses.
var handlerMock = new Mock<HttpMessageHandler>();
var magicHttpClient = new HttpClient(handlerMock.Object);
Then I took some time looking into the source code behind HttpClient.GetStringAsync()
and found it is using SendAsync()
method behind the scene.
private async Task<string> GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
{
// ...
response = await base.SendAsync(request, cts.Token).ConfigureAwait(false);
// ...
}
So we need to setup the mock like this.
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();
Now, my mock can be run successfully!
Finally, the complete unit test code for reference:
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);
}
}
}
Roger
In my case, in order to get it work, I had to set a BaseAddress to the HttpClient
ksyrium
I also had to add the BaseAddress, but this post really helped me a lot!
Ingo
Thanks for this good tutorial to mock an HttpClient.