One of the patterns in writing good unit tests is to mock dependencies. This is important so that you only test the functionality within your class and isolate anything that a dependency might perform. This is especially important when your class accesses an external resource. For example, if I have a database or web api that my class depends on, not only do I not want to have a database CRUD operation, or data-mutating web api operation executed, but these resources will not be available during test execution. Acheiving this mocking is often facilitated by Dependency Injection (DI).
Given the following simplistic/contrived code, how can you mock the HttpClient
dependency?
public class ClassUnderTest
{
private readonly HttpClient _httpClient;
private const string Url = "https://myurl";
public ClassUnderTest(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Person> GetPersonAsync(int id)
{
var response = await _httpClient.GetAsync($"{Url}?id={id}");
return await response.Content.ReadFromJsonAsync<Person>();
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
First, let's consider one solution that, though possible, isn't the best. HttpClient
does not implement an interface. You write your own interface like this:
public interface IHttpHandler
{
HttpResponseMessage Get(string url);
HttpResponseMessage Post(string url, HttpContent content);
Task<HttpResponseMessage> GetAsync(string url);
Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}
and add a class that implements it. The details of the implementation would be difficult to write in such a way that they are extensible and usable in various scenarios. Fortunately, there are already solutions out there that are freely available to you via Nuget Package. Let's walk thru a couple of Nuget Packages that I've done some POC's on using xUnit
before deciding on a single solution (Moq.Contrib.HttpClient
). Then I'll show examples of how you can use each. Note that there are many more abilities to each framework than what I show below; I kept each example succinct for clarity.
Moq (by itself)
This is relatively straightforward if you are familiar with using the Moq framework. The "trick" is to mock the HttpMessageHandler
inside of the HttpClient
- not the HttpClient
itself. NOTE: It is good practice to use MockBehavior.Strict
in the mock so that you are alerted to any calls that you have not explicitly mocked-out and were expecting.
Example:
[Fact]
public async Task JustMoq()
{
//arrange
const int personId = 1;
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = JsonContent.Create<Person>(dto)
};
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
// Inject the handler or client into your application code
var httpClient = new HttpClient(mockHandler.Object);
var sut = new ClassUnderTest(httpClient);
//act
var actual = await sut.GetPersonAsync(personId);
//assert
Assert.NotNull(actual);
mockHandler.Protected().Verify(
"SendAsync",
Times.Exactly(1),
ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>());
}
RichardSzalay.MockHttp
RichardSzalay.MockHttp is another popular solution. I've used this in the past but found it slightly more cumbersome that Moq.Contrib.HttpClient
. There are two different patterns that can be used here. Richard describes when to use one vs the other here.
Example (using BackendDefinition pattern):
[Fact]
public async Task RichardSzalayMockHttpUsingBackendDefinition()
{
//arrange
const int personId = 1;
using var mockHandler = new MockHttpMessageHandler();
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = JsonContent.Create<Person>(dto)
};
var mockedRequest = mockHandler.When(HttpMethod.Get, "https://myurl?id=1")
.Respond(mockResponse.StatusCode, mockResponse.Content);
// Inject the handler or client into your application code
var httpClient = mockHandler.ToHttpClient();
var sut = new ClassUnderTest(httpClient);
//act
var actual = await sut.GetPersonAsync(personId);
//assert
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
mockHandler.VerifyNoOutstandingRequest();
}
Example (using RequestExpectation pattern):
[Fact]
public async Task RichardSzalayMockHttpUsingRequestExpectation()
{
//arrange
const int personId = 1;
using var mockHandler = new MockHttpMessageHandler();
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = JsonContent.Create<Person>(dto)
};
var mockedRequest = mockHandler.Expect(HttpMethod.Get, "https://myurl")
.WithExactQueryString($"id={personId}")
.Respond(mockResponse.StatusCode, mockResponse.Content);
// Inject the handler or client into your application code
var httpClient = mockHandler.ToHttpClient();
var sut = new ClassUnderTest(httpClient);
//act
var actual = await sut.GetPersonAsync(personId);
//assert
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
mockHandler.VerifyNoOutstandingExpectation();
}
Moq.Contrib.HttpClient
Like the solution for using Moq
by itself, this is straightforward if you are familiar with using the Moq framework. I found this solution to be slightly more direct with less code. This is the solution I opted to use. Note that this solution requires a separate Nuget from Moq
itself - Moq.Contrib.HttpClient
Example:
[Fact]
public async Task UsingMoqContribHttpClient()
{
//arrange
const int personId = 1;
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockUrl = $"https://myurl?id={personId}";
var mockResponse = mockHandler.SetupRequest(HttpMethod.Get, mockUrl)
.ReturnsJsonResponse<Person>(HttpStatusCode.OK, dto);
// Inject the handler or client into your application code
var httpClient = mockHandler.CreateClient();
var sut = new ClassUnderTest(httpClient);
//act
var actual = await sut.GetPersonAsync(personId);
//assert
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
mockHandler.VerifyRequest(HttpMethod.Get, mockUrl, Times.Once());
}
WireMock.Net
A realtive newcomer to the game, WireMock.net is gaining popularity. This would be a reasonable solution instead of Microsoft.AspNetCore.TestHost
if you are writing Integration Tests where calls to the endpoint are actually made instead of being mocked. I thought this would be my pick at first but decided against it for two reasons:
- it does actually open ports to facilitate the test. Since I've had to fix port-exhaustion issues from improper usage of
HttpClient
in the past, I decided to pass on this solution as I wasn't sure how well it would scale across a large codebase with many unit tests running in parallel. - The urls used must be resolvable (actual legit urls). If you want the simplicity of not caring about a "real" url (just that the url you expected was actually called) then this may not be for you.
Example:
public class TestClass : IDisposable
{
private WireMockServer _server;
public TestClass()
{
_server = WireMockServer.Start();
}
public void Dispose()
{
_server.Stop();
}
[Fact]
public async Task UsingWireMock()
{
//arrange
const int personId = 1;
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockUrl = $"https://myurl?id={personId}";
_server.Given(
Request.Create()
.WithPath(mockUrl))
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBodyAsJson(dto));
// Inject the handler or client into your application code
var httpClient = _server.CreateClient();
var sut = new ClassUnderTest(httpClient);
//act
var actual = await sut.GetPersonAsync(personId);
//assert
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
}
}
No comments:
Post a Comment