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