单元测试调用了ThrowIfCancellationRequested()

时间:2014-02-24 15:51:11

标签: c# unit-testing mocking moq mstest

我目前正在使用Moq来帮助我进行单元测试,但是我遇到了一个我不知道如何解决的问题。

例如,假设我想验证CancellationToken.ThrowIfCancellationRequested()Upload(次呼叫被调用一次

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

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

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

如果token是引用类型,我通常会执行类似

的操作
[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));
}

然而,据我所知,Moq不支持值类型。测试这个的正确方法是什么,或者没有办法通过Moq做我想要的东西,而不先将CancellationToken装入容器中,然后传递给PerformUpload(

1 个答案:

答案 0 :(得分:3)

你可能已经离开了这一点,但我想到你想要测试的东西似乎并没有多大意义。测试ThrowIfCancellationRequested被称为与Upload相同的次数并不能确保它们以正确的顺序被调用,我假设在这种情况下它实际上是相关的。你不希望这样的代码通过,但我很确定它会:

_uploader.Upload(token, "Foo");

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

_uploader.Upload(token, "Bar");

正如评论中所说,解决此问题的最简单方法是将token.ThrowIfCancellationRequested电话推送到Upload电话。假设无论出于何种原因这是不可能的,我可能会采用以下方法来测试您的场景。

首先,我将封装检查是否已请求取消的功能,如果没有,则将操作调用为可测试的内容。首先想到的是,这可能是这样的:

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

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

这可以通过两次测试进行相当简单的测试。一个用于检查是否在未取消令牌时调用该操作,另一个用于验证是否取消该令牌。这些测试看起来像:

[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);
}

然后我会将IActionRunner注入UploadEngine并验证它是否被正确调用。因此,您的PerformUpload方法将更改为:

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

然后,您可以编写一对测试来验证PerformUpload。第一个检查是否已设置ActionRunner模拟执行提供的操作,然后Upload被调用至少一次。第二个测试验证如果ActionRunner模拟已设置为忽略该操作,则不会调用Upload。这基本上可以确保方法中的所有 Upload调用都通过ActionRunner完成。这些测试看起来像这样:

[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);
}

显然你可能想为你的UploadEngine编写其他测试,但它们似乎超出了当前问题的范围......