Testing with CancellationToken

The top level loop in a hosted service (worker) needs to execute while the CancellationToken has not recieved a cancellation request. This needs to be wrapped in a generic try/catch as when a BackgroundService throws an unhandled exception, the exception is lost and the service appears unresponsive. This is because the base class BackgroundService is awaiting the task to complete and return.

.NET 6 fixes this behavior by logging the exception and stopping the host.

As the code in this example was .NET 5 and I needed to test the try/catch is not removed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await _eventBroker.DoWork(cancellationToken);
}
catch (Exception e)
{
_logger.LogError($"Unexpected error: {e.Message}", );
}
}
}

This code posed two problems

  1. How does the test break out the while loop
  2. How do I test a protected method.

The second issue can be resolved by additional setup to expose protected behaviour the first needed the token to be cancelled.

There were two ways this could be done:

  • cancelling after a specific amount of time
  • mocking a dependnacy and using Callback to call the .Cancel() method on the token

Cancelling after a specific amount of time

This should work but the test could be flaky (fails to produce the same result each time)

1
2
3
4
5
6
// Arrange
var cancellationTokenSource = new CancellationTokenSource()
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5));

// Act
await classUnderTest.ExposedExecuteAsync(cancellationTokenSource);

Cancel callback

This will only be possible if you have some mocked dependency.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Arrange
var cancellationTokenSource = new CancellationTokenSource();
var someServiceMock = new Mock<ISomeService>();

someServiceMock
.Setup(x => x.SomeMethodAsync(It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("Bork city!"))
.Callback(() => {
cancellationTokenSource.Cancel();
});

// Act
await classUnderTest.ExposedExecuteAsync(cancellationTokenSource);

As the method handles the Exception the assertion could be on the ILogger else the exception will need to be expected in the unit test, with xUnit this can be handled with Assert.ThrowsAsync.

References