Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 276
Sergii Gryshkevych Avatar asked Nov 28 '19 12:11

Sergii Gryshkevych


2 Answers

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));
    }
}
like image 70
Nkosi Avatar answered Oct 10 '22 05:10

Nkosi


The best you can do without changing your code is cancelling after a specific amount of time. The CancellationTokenSource.CancelAfter() method makes this easy:

[TestCase]
public async System.Threading.Tasks.Task Run_ShallAlwaysReleaseResources()
{

  // Signal cancellation after 5 seconds
  var cts = new TestCancellationTokenSource();
  cts.CancelAfter(TimeSpan.FromSeconds(5));

  // Act
  await domainStateSerializationWorker.Run(cts.Token);  

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

The way your code is written (checking IsCancellationRequested only once per iteration) means that the cancellation will happen after some number of complete iterations. It just won't be the same number each time.

If you want to cancel after a specific number of iterations, then your only option is to modify your code to keep track of how many iterations have happened.

I thought I might be able to create a new class that inherits from CancellationTokenSource to keep track of how many times IsCancellationRequested has been tested, but it's just not possible to do.

like image 33
Gabriel Luci Avatar answered Oct 10 '22 03:10

Gabriel Luci