Reputation: 422
I want to test my httpclient post retry function in my UT, here I mock the HttpFactory:
both the first and second time, the HttpFactory always returns HttpStatusCode.InternalServerError
public class MyServiceClient
{
private readonly IHttpClientFactory _clientFactory;
public MyServiceClient(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetResponse(string test= "te")
{
using var client = _clientFactory.CreateClient("MyClient");
var content = new StringContent("{}", Encoding.UTF8, "application/json");
var response = await client.PostAsync("http://www.contoso.com/",content);
if (!response.IsSuccessStatusCode)
{
throw new ApplicationException("Application Error!");
}
var result = await response.Content.ReadAsStringAsync();
return result;
}
public async Task<string> PollyExecute()
{
try
{
var policy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(3,
count => TimeSpan.FromSeconds(2),
(ex, timeSpan,retrycount, context) =>
{
Console.WriteLine(ex);
});
var response = await policy.ExecuteAsync(()=>GetResponse());
return response;
}
catch (Exception e)
{
Console.WriteLine(e);
throw ;
}
}
}
Then I use my policy to run the client postasync method, there is no issue in my first retry, I get the excepted 500 internal server error.
public class HttpClientTest
{
[Fact]
public async Task PoliceTest()
{
var messageHandler = new StubHttpMessageHandler(HttpStatusCode.InternalServerError, "Error!!!!");
var httpClient = new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
};
var factory = Substitute.For<IHttpClientFactory>();
factory.CreateClient(Arg.Any<string>()).Returns(httpClient, httpClient);
var client = new MyServiceClient(factory);
var result = await client.PollyExecute();
}
}
public sealed class StubHttpMessageHandler : HttpMessageHandler
{
public string _response;
public HttpStatusCode _statusCode;
public StubHttpMessageHandler(HttpStatusCode statusCode, string response)
{
_statusCode = statusCode;
_response = response;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
return Task.FromResult(Send(request));
}
private HttpResponseMessage Send(HttpRequestMessage request)
{
return new HttpResponseMessage
{
Content = new StringContent(_response),
StatusCode = _statusCode,
};
}
}
However, in the second retry, when running postasync method,
It throws an exception says the httpclient is disposed.
Why? do some friends know the reason? thanks in advance!
Upvotes: 1
Views: 1840
Reputation: 606
I saw a lot of posts and finally used setupsequence instead of setup and passed new httpclient everytime
Upvotes: 0
Reputation: 1640
Firstly, you need to setup in mock a new instance of HttpClient every time.
var factory = Substitute.For<IHttpClientFactory>();
factory.CreateClient(Arg.Any<string>())
.Returns(new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
}, new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
});
Secondly, if you want to get no value without using
clause, you need to call Dispose
method manually after usage.
var client = _clientFactory.CreateClient("MyClient");
try
{
var content = new StringContent("{}", Encoding.UTF8, "application/json");
var response = await client.PostAsync("http://www.contoso.com/", content);
if (!response.IsSuccessStatusCode)
//... your whole logic
}
finally
{
if (client != null) { client.Dispose(); }
}
Upvotes: 1
Reputation: 7612
Your mock returns the same httpClient
object every time. HttpClient
once disposed cannot be reused.
Setup mock to return a new instance every time:
var factory = Substitute.For<IHttpClientFactory>();
factory.CreateClient(Arg.Any<string>()).Returns( _ => new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
});
Upvotes: 4
Reputation: 422
I added more sample code, and I get a very first investigation:
At the Polly's second time, if I remove the Using in
using var client = _clientFactory.CreateClient("MyClient");
then there is no exception. I guess it should be caused by the client's scope, but actually when executing the PostAsync() , the Client still has some values, seems it's not disposed.
It's weird.
Upvotes: 1
Reputation: 359
I think you are just mocking IHttpClientFactory
incorrectly in your test setup. By design default implementation of factory returns new client (Microsoft docs):
Each call to CreateClient(String) is guaranteed to return a new HttpClient instance. Callers may cache the returned HttpClient instance indefinitely or surround its use in a using block to dispose it when desired.
In example you provided factory returns same httpClient
that gets disposed after the first use. Same disposed object gets returned by the mocked factory again and again causing disposed error.
Just modifying your example to return different instances of the client (pardon the repeated code) avoids disposed error:
...
var httpClient = new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
};
var httpClient1 = new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
};
var httpClient2 = new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
};
var httpClient3 = new HttpClient(messageHandler)
{
BaseAddress = new Uri("http://mockuri")
};
var factory = Substitute.For<IHttpClientFactory>();
factory
.CreateClient(Arg.Any<string>())
.Returns(httpClient, httpClient1, httpClient2, httpClient3);
...
Upvotes: 7