superninja
superninja

Reputation: 3411

How to Mock HttpClient using NSubstitute

I am trying to use NSubstitute to mock HttpClient. Here's the code:

public static HttpClient GetHttpClient(bool isSucess = true, string methodType = "GET")
        {
            var mockIHttpMessageHandler = Substitute.For<IMockHttpMessageHandler>();
            var mockHttpMessageHandler = Substitute.For<MockHttpMessageHandler>(mockIHttpMessageHandler);
            var httpResponse = Substitute.For<HttpResponseMessage>();
            httpResponse.Content = new StringContent("\"test\"");
            if (isSucess)
                httpResponse.StatusCode = HttpStatusCode.OK;
            else
                httpResponse.StatusCode = HttpStatusCode.NotFound;

            var mockHttpClient = Substitute.For<HttpClient>(mockHttpMessageHandler);
            mockHttpClient.BaseAddress = new Uri("http://localhost");

            if(methodType != "POST"){
                mockHttpClient.GetAsync(Arg.Any<Uri>()).ReturnsForAnyArgs(httpResponse);
            }
            return mockHttpClient;
        }

However, I got an error at this line:

mockHttpClient.GetAsync(Arg.Any<Uri>()).ReturnsForAnyArgs(httpResponse);

And the error is

NSubstitute.Exceptions.RedundantArgumentMatcherException: 'Some argument specifications (e.g. Arg.Is, Arg.Any) were left over after the last call.

This is often caused by using an argument spec with a call to a member NSubstitute does not handle (such as a non-virtual member or a call to an instance which is not a substitute), or for a purpose other than specifying a call (such as using an arg spec as a return value). For example:

var sub = Substitute.For<SomeClass>();
var realType = new MyRealType(sub);
// INCORRECT, arg spec used on realType, not a substitute:
realType.SomeMethod(Arg.Any<int>()).Returns(2);
// INCORRECT, arg spec used as a return value, not to specify a call:
sub.VirtualMethod(2).Returns(Arg.Any<int>());
// INCORRECT, arg spec used with a non-virtual method:
sub.NonVirtualMethod(Arg.Any<int>()).Returns(2);
// CORRECT, arg spec used to specify virtual call on a substitute:
sub.VirtualMethod(Arg.Any<int>()).Returns(2);

To fix this make sure you only use argument specifications with calls to substitutes. If your substitute is a class, make sure the member is virtual.

Another possible cause is that the argument spec type does not match the actual argument type, but code compiles due to an implicit cast. For example, Arg.Any() was used, but Arg.Any() was required.

NOTE: the cause of this exception can be in a previously executed test. Use the diagnostics below to see the types of any redundant arg specs, then work out where they are being created.

Diagnostic information:

Remaining (non-bound) argument specifications: any Uri

All argument specifications: any Uri

Are they suggesting I need to change the getAsync method? There's no virtual method for GetAsync

Edit:

I have also tried to remove NSubstitute for HttpClient as follows, but I still got the same error:

public static HttpClient GetHttpClient(bool isSucess = true, string methodType = "GET")
            {
                var mockIHttpMessageHandler = Substitute.For<IMockHttpMessageHandler>();
                var mockHttpMessageHandler = Substitute.For<MockHttpMessageHandler>(mockIHttpMessageHandler);
                var httpResponse = Substitute.For<HttpResponseMessage>();
                httpResponse.Content = new StringContent("\"test\"");
                if (isSucess)
                    httpResponse.StatusCode = HttpStatusCode.OK;
                else
                    httpResponse.StatusCode = HttpStatusCode.NotFound;
    
                 var httpClient = new HttpClient(mockHttpMessageHandler);
                httpClient = new Uri("http://localhost");
    
                if(methodType != "POST"){
                    httpClient .GetAsync(Arg.Any<Uri>()).ReturnsForAnyArgs(httpResponse);
                }
                return httpClient 
            }

Upvotes: 10

Views: 14622

Answers (2)

James Law
James Law

Reputation: 6723

Having already answered this question a couple of years back, I've found myself more recently solving this problem in a slightly different way.

Instead of creating a custom handler to intercept the calls, one can substitute HttpMessageHandler directly instead.

var mockHandler = Substitute.ForPartsOf<HttpMessageHandler>();
var httpClient = new HttpClient(mockHandler);

We then use reflection to configure the substitute return, intercepting the call to the protected SendAsync, the drawback being that we forfeit strong typing because of the method access level.

var mockResponse = new HttpResponseMessage(HttpStatusCode.OK);

mockHandler.GetType().GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance)!
    .Invoke(
        mockHandler,
        [Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>()])
    .Returns(Task.FromResult(mockResponse));

var result = await httpClient.GetAsync("https://tempuri.org");

We can go a little further and mitigate the loss of strong typing by creating a simple extension method for use in our test project to provide us a public SendAsync method on all HttpMessageHandler instances.

public static class HttpMessageHandlerExtensions
{
    public static Task<HttpResponseMessage> SendAsync(this HttpMessageHandler handler, HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var sendAsync = handler.GetType()
            .GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance)!;

        return (Task<HttpResponseMessage>)
            sendAsync.Invoke(
                handler,
                [request, cancellationToken])!;
    }
}

Which can then be used to configure the mock as if SendAsync were public.

var mockResponse = new HttpResponseMessage(HttpStatusCode.OK);

mockHandler.SendAsync(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
    .Returns(mockResponse);

var result = await httpClient.GetAsync("https://tempuri.org");

Going a little further down the rabbit hole, when setting up tests for things like client APIs (think Refit etc.), I find myself mocking HttpClient as part of a ServiceCollection implementation to test the full end to end IO of the module.

We can't use the HttpMessageHandler mock we created before as IHttpClientBuilder.AddHttpMessageHandler expects an instance of DelegatingHandler instead - fortunately it's as simple as changing the type of the subbed handler. We're then able resolve the mocked HttpClient instance via IServiceProvider/IHttpClientFactory.

var mockHandler = Substitute.ForPartsOf<DelegatingHandler>();

var serviceCollection = new ServiceCollection();

serviceCollection
    .AddHttpClient("someclient")
    .AddHttpMessageHandler(() => mockHandler);

var services = serviceCollection.BuildServiceProvider();

var httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("someclient");

var mockResponse = new HttpResponseMessage(HttpStatusCode.OK);

mockHandler.SendAsync(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
    .Returns(mockResponse);

var result = await httpClient.GetAsync("https://tempuri.org");

This approach also allows for additional handlers to be configured in addition to your mock, for instance when you have a custom handler for authentication or some other manipulation of the request - the only thing to remember is to call AddHttpMessageHandler with your mock handler last of all.

Upvotes: 3

James Law
James Law

Reputation: 6723

First we need to create a mock implemenation of HttpMessageHandler. As you can see, we're overriding the protected SendAsync() method and exposing its body via our public Send() method.

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Send(request, cancellationToken);
    }

    public virtual Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

Next we need to set up our mocks. Note that I'm using Substitute.ForPartsOf<T> instead of Substitute.For<T>.

var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var httpClient = new HttpClient(mockHttpMessageHandler);

Finally, we can now use NSubstitute to intercept the call to Send() on our handler, which is called by the HttpClient for every request, and return our mocked HttpResponseMessage back via the client.

var mockResponse = new HttpResponseMessage(HttpStatusCode.OK);

mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
   .Returns(mockResponse);

var result = await httpClient.GetAsync<string>("https://tempuri.org");

.NET 6

As .NET 6 introduces a protected virtual Send() method to the HttpMessageHandler class (which will also need overriding if you're using the synchronous HttpClient calls), some modifications are required to our MockHttpMessageHandler:

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(MockSend(request, cancellationToken));
    }

    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return MockSend(request, cancellationToken);
    }

    public virtual HttpResponseMessage MockSend(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

Upvotes: 25

Related Questions