Mocking HTTP requests for unit testing is important because of the prevalence of APIs in modern software development. In this article, we will show you how to mock HttpClient and compare two ways to mock HTTP requests in our unit tests. We will use Moq to mock a HttpMessageHandler
, and also, we will analyze mocking HttpClient
by using the MockHttp NuGet package.
Let’s start.
Mock HttpMessageHandler Using Moq
Trying to unit test the code that utilizes HttpClient
, we might initially want to mock that HttpClient. Mocking HttpClient
is possible although an arduous task. Luckily there is still a great way to unit test the code. The solution is to mock HttpMessageHandler
and pass this object to the HttpClient
constructor. When we use the message handler to make HTTP requests, we achieve our unit testing goals.
By providing our own message handler we can return whatever HttpResponseMessage
we want. Using Moq
makes this process easier as we do not have to implement our own class for each response. Mocking HttpMessageHandler
is simple as there is only one, protected SendAsync()
method . Importing the Moq.Protected
namespace allows us to mock protected methods with the Protected()
method.
That said, let’s take a look at our unit test:
[TestMethod] public async Task GivenMockedHandler_WhenRunningMain_ThenHandlerResponds() { var mockedProtected = _msgHandler.Protected(); var setupApiRequest = mockedProtected.Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>() ); var apiMockedResponse = setupApiRequest.ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("mocked API response") }); }
We mock a call to the SendAsync()
function and provide a response to return with the ReturnAsync()
method. In the Setup<>()
method, the first parameter is the name of the method that is being mocked. Next, we match each parameter to the "SendAsync"
method with an expression. In this case, we are using the ItExpr.IsAny<>
method to match any HttpRequestMessage
object. Finally we, again, use ItExpr.IsAny<>
to match any CancellationToken
object.
In ReturnAsync()
, we provide an object that will be the return value of the mocked function in Setup()
. Later in this article, we will look at how we can refine our ItExpr
to match more specific parameters rather than any parameter of the right type.
Now, let’s take a look at what the code that we want to test looks like:
public static HttpMessageHandler? Handler { get; set; } public static HttpClient Client = new HttpClient(); public static async Task Main(string[] args) { if (Handler is not null) { Client = new HttpClient(Handler); } string baseAddress = "https://reqres.in"; string apiEndpoint = "/api/users/2"; await var responseMessage = Client.GetAsync(baseAddress + apiEndpoint); }
We can see we are providing the client instance handler that during a unit test will be a mocked handler. During unit testing, the value returned by GetAsync()
will be the value we provide to ReturnsAsync()
during setup.
Matching Specific HTTP Requests
While our previous example is great for testing simple code it will not suffice to test code that will call multiple API endpoints with the same HttpClient
. This is because of the way we set up the mock message handler to return the same response for any HttpRequestMessage
. To change this solution to test more complex code, we must refine each setup to match more specific parameters:
var mockedProtected = _msgHandler.Protected(); var setupApiRequest = mockedProtected.Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri!.Equals(_baseAddress + _apiEndpoint)), ItExpr.IsAny<CancellationToken>()); var apiMockedResponse = setupApiRequest .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("mocked API response") });
Using ItExpr.Is<>()
allows us to match more specific objects rather than any object of a type. In our example, we are specifically matching any HttpRequestMessage
that satisfies the m => m.RequestUri!.Equals(apiUriEndpoint)
expression. This means that the mock handler is prepared for any HTTP request to a specific API endpoint. We can further refine this setup by adding to the expression. There are other HttpRequestMessage
properties we can work with. We can try to match requests on these properties: RequestUri
, Content
, Headers
, Method
, and Version
. For example, we can create a setup with an expression that matches a certain RequestUri
, carrying a specific Content
object, and a specific request Headers
.
Mock HttpClient Using MockHttp
MockHttp
is a wonderful tool that allows us to similarly mock the message handler for HttpClient
in a more feature-complete package. Let’s explore this further after installing it into our project:
Install-Package RichardSzalay.MockHttp
We can begin using this package by adding using RichardSzalay.MockHttp
to our code.
There are two methods we can use to set up an API request: When()
and Expect()
. Calling When()
will create a backend definition. Calling Expect()
will create a request expectation.
A backend definition can be matched multiple times, but MockHttp
will not match any backend definitions when there are any outstanding request expectations. This behavior can be changed using BackendDefinitionBehavior.Always
into the MockHttpMessageHandler
constructor.
A request expectation can be matched only once and request expectations are matched in the order they were added. These two facts are the major differences in behavior compared to backend definitions. The default behavior for MockHttpMessageHandler
is to match all request expectations before matching any backend definitions. You can verify there are no outstanding request expectations by calling the VerifyNoOutstandingExpectation()
method:
_msgHandler = new MockHttpMessageHandler(); _msgHandler.When("https://reqres.in/api/users/*").Respond("text/plain", "mocked user response");
In this example, we can see one feature MockHttp
offers that mocking the message handler ourselves does not have. The wildcard character can be used to match many different API endpoints.
There are several utility methods MockHttp
offers to manage definitions and expectations. Clear()
will delete all definitions and expectations. Flush()
will complete all pending requests when the AutoFlush
property is false
. ResetBackendDefinitions()
and ResetExpections()
will clear their respective request types. Lastly, ToHttpClient()
is a great function to produce an HttpClient
that includes our mocked message handler.
Matching Specific HTTP Requests
The When()
, With()
, and Expect()
methods return MockedRequest
objects. MockedRequest
objects can be further constrained with matchers.
Let’s consider the next example where we constrain a match on an expectation:
_msgHandler.Expect("https://reqres.in/api/users/*") .WithHeaders(@"Authorization: Basic abcdef") .WithQueryString("Id=2&Version=12") .Respond("text/plain", "mocked response");
We use this expectation to add constraints on Url, headers defining Basic authorization, and the query containing specific Id and Version key-value pairs. MockHttp
offers multiple matchers that can be added to constrain a request match based on: Query, Form Data, Content, and Headers.
To learn more about MockHttp
visit the GitHub here
Conclusion
In this article, we’ve learned how to mock HttpClient in our C# code. Mocking our own message handler offers a quick and basic way to perform this mocking. It would serve as a great basis to write more complex unit testing solutions for projects. On the other hand, MockHttp
offers many great features out of the box. This enables us to write robust unit tests quickly. Learning to use a new tool can be challenging but rewarding. MockHttp
will allow us to spend more time writing robust tests rather than fighting with writing our own implementation of a mocked message handler.
Mock
HttpResponseHandlerUsing MoqMock HttpMessageHandler Using Moq
Yeah, thanks for that. It was a typo.