单元测试:如何测试ThrowIfCancellationRequested()是否被调用

6

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

例如,假设我想验证每个Upload()调用都会调用一次CancellationToken.ThrowIfCancellationRequested()

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
为什么不创建一个CancellationTokenSource并将其.Token属性传递给上传器,并使用ExpectedException属性装饰测试呢? - Pablo Romeo
因为我不是在测试取消操作是否被捕获,而是在测试模块调用与取消检查之间的1:1比率。 - Scott Chamberlain
我明白了。我不确定您是否能够模拟throwIf,因为它不是虚的。您可能需要在自己的接口中包装令牌并进行模拟。 - Pablo Romeo
你需要像Microsoft Fakes这样的框架来模拟非虚方法。http://msdn.microsoft.com/en-us/library/hh549175(v=vs.110).aspx - Tseng
你已经将令牌传递给了上传方法。为什么不直接在其中抛出异常,而是为每个调用重复呢? - EagleBeak
1个回答

5

你可能已经了解了这个问题,但是我认为你试图测试的东西似乎没有太多意义。仅测试 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编写其他测试,但它们似乎超出了当前问题的范围...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接