Reputation: 822
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
Reputation: 7712
There are several ways to unit test HttpClient, but none are straightforward because HttpClient does not implement a straightforward 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.
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.
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