单元测试时模拟 CancellationToken.IsCancellationRequested

Simulating CancellationToken.IsCancellationRequested when unit testing

我想测试一个应该 运行 一直持续到终止的任务。假设正在测试以下方法:

public class Worker
{
  public async Task Run(CancellationToken cancellationToken)
  {
    while (!cancellationToken.IsCancellationRequested)
    {
      try
      {
        // do something like claim a resource
      }
      catch (Exception e)
      {
        // catch exceptions and print to the log
      }
      finally
      {
        // release the resource
      }
    }
  }
}

还有一个测试用例

[TestCase]
public async System.Threading.Tasks.Task Run_ShallAlwaysReleaseResources()
{
  // Act
  await domainStateSerializationWorker.Run(new CancellationToken());  

  // Assert
  // assert that resource release has been called
}

问题是任务永远不会终止,因为从未请求取消。最终我想创建一个像 MockRepository.GenerateStub<CancellationToken>() 这样的 CancellationToken 存根并告诉它在哪个调用 IsCancellationRequested return true 上,但是 CancellationToken 不是引用类型,因此不可能。

所以问题是如何进行 Run 执行 n 次迭代然后终止的测试?不重构可以吗Run

在不更改代码的情况下,您最多只能在特定时间后取消。 CancellationTokenSource.CancelAfter() 方法使这变得简单:

[TestCase]
public async System.Threading.Tasks.Task Run_ShallAlwaysReleaseResources()
{

  // Signal cancellation after 5 seconds
  var cts = new TestCancellationTokenSource();
  cts.CancelAfter(TimeSpan.FromSeconds(5));

  // Act
  await domainStateSerializationWorker.Run(cts.Token);  

  // Assert
  // assert that resource release has been called
}

您的代码编写方式(每次迭代仅检查 IsCancellationRequested 一次)意味着取消将在 一些 次完整迭代后发生。只是不会每次都是相同的数字。

如果您想在特定次数的迭代后取消,那么您唯一的选择是修改您的代码以跟踪发生了多少次迭代。

我想我可以创建一个继承自 CancellationTokenSource 的新 class 来跟踪 IsCancellationRequested 已经测试了多少次,但这是不可能的做。

这取决于Run中的运行。如果有一些注入依赖

例如

public interface IDependency {
    Task DoSomething();
}

public class Worker {
    private readonly IDependency dependency;

    public Worker(IDependency dependency) {
        this.dependency = dependency;
    }

    public async Task Run(CancellationToken cancellationToken) {
        while (!cancellationToken.IsCancellationRequested) {
            try {
                // do something like claim a resource
                await dependency.DoSomething();
            } catch (Exception e) {
                // catch exceptions and print to the log
            } finally {
                // release the resource
            }
        }
    }
}

然后可以对其进行模拟和监控,以计算某个成员被调用的次数。

[TestClass]
public class WorkerTests {
    [TestMethod]
    public async Task Sohuld_Cancel_Run() {
        //Arrange
        int expectedCount = 5;
        int count = 0;
        CancellationTokenSource cts = new CancellationTokenSource();
        var mock = new Mock<IDependency>();
        mock.Setup(_ => _.DoSomething())
            .Callback(() => {
                count++;
                if (count == expectedCount)
                    cts.Cancel();
            })
            .Returns(() => Task.FromResult<object>(null));

        var worker = new Worker(mock.Object);

        //Act
        await worker.Run(cts.Token);

        //Assert
        mock.Verify(_ => _.DoSomething(), Times.Exactly(expectedCount));
    }
}