Using Cooperative Cancellation in long-running tests

NUnit 4 has added support for cooperatively cancelling long-running tests in several scenarios. Cooperative cancellation is the preferred way to end any long-running operation in .NET as it allows for the graceful ending of operations, however it requires explicit coordination by calling code to do this. This coordination is handled, in part, by the passing of a CancellationToken into the potentially long-running method. The cancellation token can be signaled to a “cancelled” state to allow the long-running operation to react and gracefully end itself.

For example, the below code which must get an external resource over HTTP where a cancellationToken is passed in as the last parameter. This GetAsync() method will exit early when the operation is cancelled.

        var httpClient = new HttpClient();
        await httpClient.GetAsync("https://server", cancellationToken);

But where does this cancellationToken come from? It’s possible to construct the token and manage it from a CancellationTokenSource yourself, however frameworks will often have a mechanism to do this for you.

NUnit supports cooperative cancellation in a few ways, the simplest of which is through the CancelAfter attribute. This attribute will indicate to NUnit that it should manage a cancellation token on behalf of the test. The cancellation token itself can be used by the test in one of two ways:

Read from the test context:

        [Test]
        [CancelAfter(CooperativeTimeoutMilliseconds)]
        public async Task WithCooperativeCancellation_Context()
        {
            var delay = TimeSpan.FromMilliseconds(600);
            var timer = Stopwatch.StartNew();
            await Task.Delay(delay, TestContext.CurrentContext.CancellationToken);
            timer.Stop();

            var expectedDelay = Math.Min(delay.Milliseconds, CooperativeTimeoutMilliseconds);

            Assert.That(timer.ElapsedMilliseconds, Is.EqualTo(expectedDelay).Within(50));
        }

Passed by NUnit as an argument into the method:

        [Test]
        [CancelAfter(CooperativeTimeoutMilliseconds)]
        public async Task WithCooperativeCancellation_Argument(CancellationToken cancellationToken)
        {
            var delay = TimeSpan.FromMilliseconds(600);
            var timer = Stopwatch.StartNew();
            await Task.Delay(delay, cancellationToken);
            timer.Stop();

            var expectedDelay = Math.Min(delay.Milliseconds, CooperativeTimeoutMilliseconds);

            Assert.That(timer.ElapsedMilliseconds, Is.EqualTo(expectedDelay).Within(50));
        }

Both conventions are supported by NUnit and will allow a long-running test to respond to and gracefully cancel any long-running tasks in flight.

Comments

Leave a Reply

Discover more from Software by Steven

Subscribe now to keep reading and get access to the full archive.

Continue reading