调用了单元测试ThrowIfCancellationRequested()

Sco*_*ain 5 c# unit-testing mstest moq mocking

我目前正在使用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");
}
Run Code Online (Sandbox Code Playgroud)

如果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));
}
Run Code Online (Sandbox Code Playgroud)

但是据我所知,最小起订量不支持值类型。什么是测试此方法的正确方法,或者没有在不CancellationToken首先将内部装箱的容器装箱之前通过Moq做我想做的事情PerformUpload(

for*_*rir 5

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

_uploader.Upload(token, "Foo");

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

_uploader.Upload(token, "Bar");
Run Code Online (Sandbox Code Playgroud)

正如评论中所说,解决这个问题的最简单方法是将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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这可以通过两个测试进行相当简单的测试。如果令牌未被取消,则检查该操作被调用,如果被取消则验证它不是。这些测试看起来像:

[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);
}
Run Code Online (Sandbox Code Playgroud)

然后我会注入IActionRunnerUploadEngine并验证它被称为正确。因此,您的PerformUpload方法将更改为:

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

然后,您可以编写一对测试来验证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);
}
Run Code Online (Sandbox Code Playgroud)

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