Reputation: 3411
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
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
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");
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