【问题标题】:Unit testing ThrowIfCancellationRequested() was called单元测试 ThrowIfCancellationRequested() 被调用
【发布时间】:2014-03-26 09:23:11
【问题描述】:

我目前正在使用 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 不支持值类型。什么是测试这个的正确方法,或者如果不将CancellationToken 装箱在一个容器内首先传递给PerformUpload(,就没有办法通过 Moq 做我想做的事情?

【问题讨论】:

  • 为什么不创建一个 CancellationTokenSource 并将其 .Token 属性传递给上传者并使用 ExpectedException 属性装饰测试?
  • 因为我没有测试如果取消它会被抓住。我正在测试模块调用与取消检查的比例是 1:1。
  • 我明白了。我不确定你是否能够模拟 throwIf 因为它不是虚拟的。您可能必须将令牌包装在您自己的界面中并模拟它。
  • 你需要一个像 Microsoft Fakes 这样的框架来模拟非虚拟方法。 msdn.microsoft.com/en-us/library/hh549175(v=vs.110).aspx
  • 您已经将令牌传递给 Upload 方法。为什么不直接从其中抛出异常,而不是为每次调用复制它?

标签: c# unit-testing mocking moq mstest


【解决方案1】:

您可能已经从这里开始,但我突然想到您尝试测试的内容似乎没有任何意义。测试ThrowIfCancellationRequested 的调用次数与Upload 的调用次数相同,并不能确保它们以正确的顺序被调用,我假设这在这种情况下实际上是相关的。你不希望这样的代码通过,但我很确定它会:

_uploader.Upload(token, "Foo");

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

_uploader.Upload(token, "Bar");

正如 cmets 中所说,解决此问题的最简单方法是将 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 编写其他测试,但它们似乎超出了当前问题的范围...

【讨论】:

    猜你喜欢
    • 2016-08-07
    • 2018-09-30
    • 2018-08-31
    • 1970-01-01
    • 2012-03-06
    • 2018-08-06
    • 2023-03-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多