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.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!