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
?
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));
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With