Kal
Kal

Reputation: 141

Polly verify how many times httpclient timed out/retried in a Unit Test

This is not a fully working solution, just parts of the code to make the question cleaner and more readable.

I have read the Polly's documentation here, however I really need to test whether the following TimeoutPolicy's onTimeoutAsync delegate is being hit (also, in case of wrapping TimeoutPolicy along with RetryPolicy - how many TIMES it has been hit) after the httpClient times out:

public static TimeoutPolicy<HttpResponseMessage> TimeoutPolicy
{
    get
    {
        return Policy.TimeoutAsync<HttpResponseMessage>(1, onTimeoutAsync: (context, timeSpan, task) =>
        {
            Console.WriteLine("Timeout delegate fired after " + timeSpan.TotalMilliseconds);
            return Task.CompletedTask;
        });
    }
}

TimeoutPolicy is set to timeout after 1 second of not receiving anything from HTTP client (HttpClient's mock is delayed for 4 seconds as shown below)

var httpMessageHandler = new Mock<HttpMessageHandler>();

httpMessageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Callback(() => Thread.Sleep(4000))//Delayed here
.Returns(() => Task.FromResult(new HttpResponseMessage
{
    StatusCode = HttpStatusCode.InternalServerError,
    Content = new StringContent("Some response body", Encoding.UTF8, "text/xml")
}));

var httpClient = new HttpClient(httpMessageHandler.Object);

Policies are mocked to be able to call .Verify() on it for Assertion:

var mockedPolicies = new Mock<Policies>();

I execute the call with a mocked HTTP Client and a mocked Timeout policy:

await mockedPolicies.Object.TimeoutPolicy.ExecuteAsync(ct => _httpClient.PostAsync("", new StringContent("Some request body"), ct), CancellationToken.None);

Assertion:

mockedPolicies.Verify(p => p.TimeoutDelegate(It.IsAny<Context>(), It.IsAny<TimeSpan>(), It.IsAny<Task>()), Times.Exactly(1));

However, test outcome says it has been called 0 times instead of 1. Thank you for any answers.

Upvotes: 2

Views: 2513

Answers (1)

mountain traveller
mountain traveller

Reputation: 8156

The issue is that the test code posted does not return a cancellable Task, at the value mocked for SendAsync to return. In that aspect it is not mirroring the way HttpClient.SendAsync(...) behaves.

And Polly Timeout policy with the default TimeoutStrategy.Optimistic operates by timing-out CancellationToken, so the delegates you execute must respond to co-operative cancellation.


Detail:

All async calls run synchronously until the first await statement. In the posted test code, both:

// [1]
.Callback(() => Thread.Sleep(4000))//Delayed here

and:

// [2]
.Returns(() => Task.FromResult(new HttpResponseMessage
{
    StatusCode = HttpStatusCode.InternalServerError,
    Content = new StringContent("Some response body", Encoding.UTF8, " text/xml")
})

contain no await statement. So [1] and [2] will run fully synchronously on the calling thread (for the full four seconds), until they return the completed Task returned by Task.FromResult. At no point do they return a 'hot' (running) Task which TimeoutPolicy can govern - the calling policy doesn't regain control until the synchronous four-second execution has completed. And [1] and [2] don't respond to any CancellationToken. So the calling timeout policy can never time it out.

Afaik there is no CallbackAsync(...) in Moq at this time, so you need to move the Thread.Sleep into the Returns(() => ) lambda expression, as a cancellable await Task.Delay(...), to simulate the way HttpClient.SendAsync(...) behaves. Something like this:

httpMessageHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", 
        ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
    .Returns<object, CancellationToken>(async (request, cancellationToken) => {
        await Task.Delay(4000, cancellationToken); // [3]
        return new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.InternalServerError,
            Content = new StringContent("Some response body", Encoding.UTF8, "text/xml")
        };
    });

In this code, TimeoutPolicy regains control at the await at [3], and the cancellationToken has control to cancel the running Task.

Upvotes: 5

Related Questions