11

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?

Sergii Gryshkevych
  • 4,029
  • 1
  • 25
  • 42
  • 1
    something like`var tokenSource = new CancellationTokenSource(); Task.Run(async () => await Task.Delay(1000); tokenSource.Cancel()); await domainStateSerializationWorker.Run(tokenSource.Token);` will cancel after 1s – Selvin Nov 28 '19 at 12:29
  • so termination after n iteration is is not possible but after some defined time it is – Selvin Nov 28 '19 at 12:35
  • @Selvin With this approach test result would be nondeterministic. I want `Run` to run for `x`iterations, not `x` seconds – Sergii Gryshkevych Nov 28 '19 at 12:35
  • you can mock some object where you are storaing result of work and cancel from it (if in where `do something like claim a resource` you have something like `observer.SendResult(...);` then you can try to mock `observer.SendResult` and cancel token after n calls) .... **without more code/context question is not answerable** – Selvin Nov 28 '19 at 12:36
  • You might be able to get there with some weird Reflection code, but refactoring `Run` - even if you initially didn't want to do it - is probably the most (cost) effective way to get what you want. – Markus Deibel Nov 28 '19 at 12:59
  • @SergiiGryshkevych This depends on what is running within Run. Is there some injected dependency that can be mocked and monitored to count how many times some member has been invoked? – Nkosi Nov 28 '19 at 13:34

2 Answers2

19

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));
    }
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
8

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.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • *wait 6 just to be safe* don't you worry.... It will be never called... Also IsCancellationRequested is a property of CancellationToken struct not CancellationTokenSource class – Selvin Nov 28 '19 at 18:38
  • @Selvin Yeah I guess `Run` won't return until the cancellation happens. My bad. I fixed that. But also, `IsCancellationRequested` is a proptery of both, and `CancellationToken.IsCancellationRequested` just [returns the value from `CancellationTokenSource.IsCancellationRequested`](https://referencesource.microsoft.com/#mscorlib/system/threading/CancellationToken.cs,86). – Gabriel Luci Nov 28 '19 at 18:45