Search code examples
c#unit-testingasynchronousrhino-mocks

Simulating CancellationToken.IsCancellationRequested when unit testing


I would like to test a task that is supposed to run continuously until killed. Suppose the following method is being tested:

public class Worker
{
  public async Task Run(CancellationToken cancellationToken)
  {
    while (!cancellationToken.IsCancellationRequested)
    {
      try
      {
        // do something like claim a resource
      }
      catch (Exception e)
      {
        // catch exceptions and print to the log
      }
      finally
      {
        // release the resource
      }
    }
  }
}

And a test case

[TestCase]
public async System.Threading.Tasks.Task Run_ShallAlwaysReleaseResources()
{
  // Act
  await domainStateSerializationWorker.Run(new CancellationToken());  

  // Assert
  // assert that resource release has been called
}

The problem is that the task never terminates, because cancellation is never requested. Ultimately I would like to create a CancellationToken stub like MockRepository.GenerateStub<CancellationToken>() and tell it on which call to IsCancellationRequested return true, but CancellationToken is not a reference type so it is not possible.

So the question is how to make a test where Run executes for n iterations and then terminates? Is it possible without refactoring Run?


Solution

  • This depends on what is running within Run. If there is some injected dependency

    For example

    public interface IDependency {
        Task DoSomething();
    }
    
    public class Worker {
        private readonly IDependency dependency;
    
        public Worker(IDependency dependency) {
            this.dependency = dependency;
        }
    
        public async Task Run(CancellationToken cancellationToken) {
            while (!cancellationToken.IsCancellationRequested) {
                try {
                    // do something like claim a resource
                    await dependency.DoSomething();
                } catch (Exception e) {
                    // catch exceptions and print to the log
                } finally {
                    // release the resource
                }
            }
        }
    }
    

    Then that can be mocked and monitored to count how many times some member has been invoked.

    [TestClass]
    public class WorkerTests {
        [TestMethod]
        public async Task Sohuld_Cancel_Run() {
            //Arrange
            int expectedCount = 5;
            int count = 0;
            CancellationTokenSource cts = new CancellationTokenSource();
            var mock = new Mock<IDependency>();
            mock.Setup(_ => _.DoSomething())
                .Callback(() => {
                    count++;
                    if (count == expectedCount)
                        cts.Cancel();
                })
                .Returns(() => Task.FromResult<object>(null));
    
            var worker = new Worker(mock.Object);
    
            //Act
            await worker.Run(cts.Token);
    
            //Assert
            mock.Verify(_ => _.DoSomething(), Times.Exactly(expectedCount));
        }
    }