Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing ThrowIfCancellationRequested() was called

I am currently using Moq to help with my unit testing however I ran in to an issue that I do not know how to resolve.

For example, say I would like to validate that CancellationToken.ThrowIfCancellationRequested() was called once per Upload( call

public UploadEngine(IUploader uploader)
{
     _uploader = uploader;
}

public void PerformUpload(CancellationToken token)
{
    token.ThrowIfCancellationRequested();
    _uploader.Upload(token, "Foo");

    token.ThrowIfCancellationRequested();
    _uploader.Upload(token, "Bar");
}

If token was a reference type I normally would do something like

[TestMethod()]
public void PerformUploadTest()
{
    var uploader = new Mock<IUploader>();
    var token = new Mock<CancellationToken>();

    int callCount = 0;

    uploader.Setup(a => a.Upload(token.Object, It.IsAny<string>())).Callback(() => callCount++);
    token.Setup(a => a.ThrowIfCancellationRequested());

    var engine = new UploadEngine(uploader.Object);
    engine.PerformUpload(token.Object);

    token.Verify(a => a.ThrowIfCancellationRequested(), Times.Exactly(callCount));
}

However from what I can tell Moq does not support value types. What would be the correct way to test this, or is there no way to do what I want through Moq without boxing the CancellationToken inside a container first to be passed in to PerformUpload(?

like image 907
Scott Chamberlain Avatar asked Feb 24 '14 15:02

Scott Chamberlain


People also ask

Should I call ThrowIfCancellationRequested?

Yes, you are supposed to call ThrowIfCancellationRequested() manually, in the appropriate places in your code (where appropriate is determined by you as a programmer). Consider the following example of a simple job processing function that reads jobs from a queue and does stuff with them.

What is ThrowIfCancellationRequested?

ThrowIfCancellationRequested , however, will set the Canceled condition of the task, causing the ContinueWith to fire. Note: This is only true when the task is already running and not when the task is starting. This is why I added a Thread. Sleep() between the start and cancellation.

What is CancellationToken used for in C#?

A CancellationToken enables cooperative cancellation between threads, thread pool work items, or Task objects. You create a cancellation token by instantiating a CancellationTokenSource object, which manages cancellation tokens retrieved from its CancellationTokenSource.

How does cancellation token works?

You can use CancellationTokens to stop async tasks as well. In an async task, cancellation indicates that the task should cease performing its current activity. An async task receives a cancellation token and examines it to see if a cancellation is requested. If so, the current operation should cease immediately.


1 Answers

You've probably moved on from this, but it occurred to me that what you're trying to test doesn't really seem to make sense. Testing that ThrowIfCancellationRequested is called the same number of times as Upload does nothing to ensure that they have been called in the right sequence, which I'm assuming would actually be relevant in this case. You wouldn't want code like this to pass, but I'm pretty sure it would:

_uploader.Upload(token, "Foo");

token.ThrowIfCancellationRequested();
token.ThrowIfCancellationRequested();

_uploader.Upload(token, "Bar");

As has been said in the comments, the easiest way to get round this would be to push the token.ThrowIfCancellationRequested call into the Upload call. Assuming that for whatever reason this isn't possible, I might take the following approach to testing your scenario.

Firstly, I'd encapsulate the functionality of checking to see if the cancellation had been requested and if not, calling an action into something testable. At first thought, this might look like this:

public interface IActionRunner {
    void ExecIfNotCancelled(CancellationToken token, Action action);
}

public class ActionRunner : IActionRunner{
    public void ExecIfNotCancelled(CancellationToken token, Action action) {
        token.ThrowIfCancellationRequested();
        action();
    }
}

This can be fairly trivially tested with two tests. One to check that action is called if the token isn't cancelled and one to validate that it isn't if it is cancelled. These tests would look like:

[TestMethod]
public void TestActionRunnerExecutesAction() {
    bool run = false;
    var runner = new ActionRunner();
    var token = new CancellationToken();

    runner.ExecIfNotCancelled(token, () => run = true);

    // Validate action has been executed
    Assert.AreEqual(true, run);
}

[TestMethod]
public void TestActionRunnerDoesNotExecuteIfCancelled() {
    bool run = false;
    var runner = new ActionRunner();
    var token = new CancellationToken(true);

    try {
        runner.ExecIfNotCancelled(token, () => run = true);
        Assert.Fail("Exception not thrown");
    }
    catch (OperationCanceledException) {
        // Swallow only the expected exception
    }
    // Validate action hasn't been executed
    Assert.AreEqual(false, run);
}

I would then inject the IActionRunner into the UploadEngine and validate that it was being called correctly. So, your PerformUpload method would change to:

public void PerformUpload(CancellationToken token) {
    _actionRunner.ExecIfNotCancelled(token, () => _uploader.Upload(token, "Foo"));
    _actionRunner.ExecIfNotCancelled(token, () => _uploader.Upload(token, "Bar"));
}

You can then write a pair of tests to validate PerformUpload. The first checks that if an ActionRunner mock has been setup to execute the supplied action then Upload is called at least once. The second test validates that if the ActionRunner mock has been setup to ignore the action, then Upload isn't called. This essentially ensures that all Upload calls in the method are done via the ActionRunner. These tests look like this:

[TestMethod]
public void TestUploadCallsMadeThroughActionRunner() {
    var uploader = new Mock<IUploader>();
    var runner = new Mock<IActionRunner>();
    var token = new CancellationToken();

    int callCount = 0;

    uploader.Setup(a => a.Upload(token, It.IsAny<string>())).Callback(() => callCount++);
    // Use callback to invoke actions supplied to runner
    runner.Setup(x => x.ExecIfNotCancelled(token, It.IsAny<Action>()))
          .Callback<CancellationToken, Action>((tok,act)=>act());

    var engine = new UploadEngine(uploader.Object, runner.Object);
    engine.PerformUpload(token);

    Assert.IsTrue(callCount > 0);
}

[TestMethod]
public void TestNoUploadCallsMadeThroughWithoutActionRunner() {
    var uploader = new Mock<IUploader>();
    var runner = new Mock<IActionRunner>();
    var token = new CancellationToken();

    int callCount = 0;

    uploader.Setup(a => a.Upload(token, It.IsAny<string>())).Callback(() => callCount++);
    // NOP callback on runner prevents uploader action being run
    runner.Setup(x => x.ExecIfNotCancelled(token, It.IsAny<Action>()))
          .Callback<CancellationToken, Action>((tok, act) => { });

    var engine = new UploadEngine(uploader.Object, runner.Object);
    engine.PerformUpload(token);

    Assert.AreEqual(0, callCount);
}

There would obviously be other tests that you might want to write for your UploadEngine but they seem out of scope for the current question...

like image 96
forsvarir Avatar answered Oct 24 '22 05:10

forsvarir