8

I'm recently learning and using Polly to add resilience to my code, especially for the timeout and retry policies. However, I don't know how to unit test the code with polly. To be more specific, I don't know how to mock a method with Cancellation Token as its parameter. Below is the structure of my code

public class Caller
{
    private IHttpManager httpManager;
    private IAsyncPolicy<HttpResponseMessage> policyWrap;

    public Caller(IHttpManager httpManager, IAsyncPolicy<HttpResponseMessage> policyWrap)
    {
        this.httpManager= httpManager;
        this.policyWrap = policyWrap;
    }

    public async Task CallThirdParty()
    {      
        HttpResponseMessage httpResponse = await policyWrap.ExecuteAsync(async ct => await httpManager.TryCallThirdParty(ct), CancellationToken.None);
    }
}

public interface IHttpManager
{
    Task<HttpResponseMessage> TryCallThirdParty(CancellationToken cancellationToken);
}

Below is the unit test I intend to run but don't know how.

[Test]
public void TestBehaviourUnderTimeoutPolicy()
{
     // Set up the timeout policy such that the governed delegate will terminate after 1 sec if no response returned
     AsyncTimeoutPolicy<HttpResponseMessage> timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(1, TimeoutStrategy.Optimistic);

     // mock IHttpManager
     var mockHttpManager= new Mock<IHttpManager>();

     // THIS IS WHERE I'M HAVING TROUBLE WITH.
     // I want to simulate the behaviour of the method such that
     // it will throw an exception whenever the CancellationToken passed from the polly caller expires
     // But how can I do that with mock?
     mockHttpManager.Setup(m => m.TryCallThirdParty(It.IsAny<CancellationToken>()))).Returns(Task.Run(() => { Thread.Sleep(10000); return new HttpResponseMessage(); }));

     Caller c = new Caller(mockHttpManager.Object, timeoutPolicy);
     await c.CallThirdParty();         
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Jane.L
  • 333
  • 1
  • 5
  • 13
  • You want a `Task` that will always fail (use [`Task.FromCancelled()`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.fromcanceled?view=netframework-4.7.2) or to return either completed `Task` or cancelled `Task` depending on parameter? Either way use [`Returns(Func)` (search 'access invocation arguments when returning a value'](https://github.com/Moq/moq4/wiki/Quickstart) overload. – orhtej2 Apr 02 '19 at 20:25
  • @orhtej2 I don't think I want a Task that will always fail. Instead, I want it to throw an exception once the CancellationToken is flagged in the middle of the method execution. Otherwise, it will just wait – Jane.L Apr 02 '19 at 20:38
  • 1
    Seems you're trying to unit test IAsyncPolicy and Caller at the same time. Both IHttpManager and IAsyncPolicy should be mocked and the mocked IHttpManager.TryCallThirdParty should throw a OperationCanceledException, or whatever exception you're expecting, that way you can ensure your logic handles the exception appropriately. To test Polly's timeout policy itself try writing functional tests for IAsyncPolicy by not mocking anything and making sure you get the result you expect according to the policy you set. You could even write a functional test for what you have here. – Noremac Apr 02 '19 at 21:13

3 Answers3

11

@Noremac is right (you test 2 elements in one unit test).

However, to answer the question "How to make Mock<T> throw an exception when CancellationToken is cancelled": Use Returns<TParam1, ...>(Func<TResult, TParam1>) overload. I replaced AsyncTimeoutPolicy with CancellationTokenSource for clarity.

// This test will perma-hang if the exception is not thrown
[TestMethod]
[ExpectedException(typeof(OperationCanceledException))]
public async Task TestMethod1()
{
    var source = new Mock<IHttpManager>();
    source.Setup(s => s.TryCallThirdParty(It.IsAny<CancellationToken>())).Returns<CancellationToken>(
        async token =>
        {
            // Wait until the token is cancelled
            await Task.Delay(Timeout.Infinite, token);
        });

    var tcs = new CancellationTokenSource(1000);
    await new Caller().Get(source.Object, tcs.Token);
}
orhtej2
  • 2,133
  • 3
  • 16
  • 26
  • Thank you very much for the response. I actually don't want to test the functionality of polly because it is a third party library and probably already very well tested before the release. I just want to test how my Caller class behave with a polly. Having said that, is it a bad practice to only mock one element (IHttpManager) while injecting a real object for the other (IAsyncPolicy policyWrap)? – Jane.L Apr 02 '19 at 21:43
  • IMO that turns unit test more towards integration test, unless the class you're providing is meant to use only in this context. – orhtej2 Apr 02 '19 at 21:57
1

Testing the functionality like there were no policy/policies

In this case you can pass a NoOpPolicy as the policyWrap parameter

//Arrange
var managerMock = new Mock<IHttpManager>();
IAsyncPolicy<HttpResponseMessage> policy = Policy.NoOpAsync<HttpResponseMessage>();

var sut = new Caller(managerMock.Object, policy);

//Act
await sut.CallThirdParty();

//Assert 
//...

Making sure that the policy has been used

In this case you can simple create a mock for IAsyncPolicy and call the Verify

//Arrange
var managerMock = new Mock<IHttpManager>();
var policyMock = new Mock<IAsyncPolicy<HttpResponseMessage>>();

var sut = new Caller(managerMock.Object, policyMock.Object);

//Act
await sut.CallThirdParty();

//Asssert
policyMock.Verify(p => p.ExecuteAsync(It.IsAny<Func<CancellationToken, Task<HttpResponseMessage>>>(), It.IsAny<CancellationToken>()), Times.Once);

Making sure that exception is not swallowed

In this case you can use ThrowsAsync calls

//Arrange
var managerMock = new Mock<IHttpManager>();
var policyMock = new Mock<IAsyncPolicy<HttpResponseMessage>>();
policyMock
    .Setup(p => p.ExecuteAsync(It.IsAny<Func<CancellationToken, Task<HttpResponseMessage>>>(), It.IsAny<CancellationToken>()))
    .ThrowsAsync(new TimeoutRejectedException());

var sut = new Caller(managerMock.Object, policyMock.Object);

//Act + Assert
await Assert.ThrowsAsync<TimeoutRejectedException>(async () => await sut.CallThirdParty());

Making sure that if result is received then there is no exception

In this case you can use the Record structure

//Arrange
var managerMock = new Mock<IHttpManager>();
var policyMock = new Mock<IAsyncPolicy<HttpResponseMessage>>();
policyMock
    .Setup(p => p.ExecuteAsync(It.IsAny<Func<CancellationToken, Task<HttpResponseMessage>>>(), It.IsAny<CancellationToken>()))
    .ReturnsAsync(new HttpResponseMessage());

var sut = new Caller(managerMock.Object, policyMock.Object);

//Act 
var exception  = await Record.ExceptionAsync(async () => await sut.CallThirdParty());

//Asssert
Assert.Null(exception);
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
0

your code snipped is never calling this method "c.TryCallThirdParty".

Did you tried to use Task.Delay(Timespan.FromSeconds(1)) instead of Thread.Sleep? Its every time better to use Task.Delay instead of Thread.Sleep.

Changer
  • 85
  • 1
  • 7