Jordan1993
Jordan1993

Reputation: 822

How to unit test HttpClient GetAsync

I have the following in my controller:

public async Task<IActionResult> IndexAsync()
{
    string baseUrl = "https://apilink.com";
    
    using (HttpClient client = new HttpClient())
    using (HttpResponseMessage response = await client.GetAsync(baseUrl))
    using (HttpContent content = response.Content)
    {
        string data = await content.ReadAsStringAsync();
        if (data != null)
        {
            var recipeList = JsonConvert.DeserializeObject<Recipe[]>(data);
            return View();
        }
    }

    return View();
}

I want to unit test this but cannot work out how to test the HttpClient.

I have tried:

[Test]
public void Index_OnPageLoad_AllRecipesLoaded()
{ 
    var testController = new HomeController();
    mockHttpClient.Setup(m => m.GetAsync(It.IsAny<string>())).Returns(
        () => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
    mockHttpContent.Setup(m => m.ReadAsStringAsync()).Returns(() => Task.FromResult(LoadJson()));
    var result = testController.IndexAsync();
    Assert.IsNotNull(result); 
}

// Loads the Json data as I don't actually want to make the API call in the test. 
public string LoadJson()
{
    using (StreamReader r = new StreamReader("testJsonData.json"))
    {
        string json = r.ReadToEnd();
        return json;
    }
}

Is there a way to mock this effectively/simply? Or should I maybe inject my own IHttpClient interface? (I am not sure if that is good practice?)

Thanks

Upvotes: 0

Views: 2015

Answers (1)

Christian Findlay
Christian Findlay

Reputation: 7712

There are several ways to unit test HttpClient, but none are straightforward because HttpClient does not implement a straightforward abstraction.

1) Write an abstraction

Here is a straightforward abstraction and you can use this instead of HttpClient. This is my recommended approach. You can inject this into your services and mock the abstraction with Moq as you have done above.

public interface IClient
{
    /// <summary>
    /// Sends a strongly typed request to the server and waits for a strongly typed response
    /// </summary>
    /// <typeparam name="TResponseBody">The expected type of the response body</typeparam>
    /// <param name="request">The request that will be translated to a http request</param>
    /// <returns>The response as the strong type specified by TResponseBody /></returns>
    /// <typeparam name="TRequestBody"></typeparam>
    Task<Response<TResponseBody>> SendAsync<TResponseBody, TRequestBody>(IRequest<TRequestBody> request);

    /// <summary>
    /// Default headers to be sent with http requests
    /// </summary>
    IHeadersCollection DefaultRequestHeaders { get; }

    /// <summary>
    /// Base Uri for the client. Any resources specified on requests will be relative to this.
    /// </summary>
    AbsoluteUrl BaseUri { get; }
}

Full code reference here. The Client class implements the abstraction.

2) Create a Fake Http Server and Verify the Calls on the Server-Side

This code sets up a fake server, and your tests can verify the Http calls.

    using var server = ServerExtensions
        .GetLocalhostAddress()
        .GetSingleRequestServer(async (context) =>
    {
Assert.AreEqual("seg1/", context?.Request?.Url?.Segments?[1]);
Assert.AreEqual("seg2", context?.Request?.Url?.Segments?[2]);
Assert.AreEqual("?Id=1", context?.Request?.Url?.Query);
Assert.AreEqual(headerValue, context?.Request?.Headers[headerKey]);

if (hasRequestBody)
{
    var length = context?.Request?.ContentLength64;
    if (!length.HasValue) throw new InvalidOperationException();
    var buffer = new byte[length.Value];
    _ = (context?.Request?.InputStream.ReadAsync(buffer, 0, (int)length.Value));
    Assert.AreEqual(requestJson, Encoding.UTF8.GetString(buffer));
}

if (context == null) throw new InvalidOperationException();

await context.WriteContentAndCloseAsync(responseJson).ConfigureAwait(false);
});

Full code reference here.

3) Mock the HttpHandler

You can inject a Mock HttpHandler into the HttpClient. Here is an example:

private static void GetHttpClientMoq(out Mock<HttpMessageHandler> handlerMock, out HttpClient httpClient, HttpResponseMessage value)
{
    handlerMock = new Mock<HttpMessageHandler>();

    handlerMock
    .Protected()
    .Setup<Task<HttpResponseMessage>>(
    "SendAsync",
    ItExpr.IsAny<HttpRequestMessage>(),
    ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(value)
    .Verifiable();

    httpClient = new HttpClient(handlerMock.Object);
}

Full code reference. You can then verify the calls from the mock itself.

Upvotes: 3

Related Questions