【问题标题】:Simulating CancellationToken.IsCancellationRequested when unit testing单元测试时模拟CancellationToken.IsCancellationRequested
【发布时间】:2020-03-24 02:24:26
【问题描述】:

我想测试一个应该连续运行直到被杀死的任务。假设正在测试以下方法:

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 的哪个调用返回true,但CancellationToken 不是引用类型,所以这是不可能的。

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

【问题讨论】:

  • var tokenSource = new CancellationTokenSource(); Task.Run(async () => await Task.Delay(1000); tokenSource.Cancel()); await domainStateSerializationWorker.Run(tokenSource.Token); 之类的东西会在 1 秒后取消
  • 所以在 n 次迭代后终止是不可能的,但在某个定义的时间后它是
  • @Selvin 使用这种方法,测试结果将是不确定的。我希望 Run 运行 xiterations,而不是 x
  • 您可以模拟一些存储工作结果的对象并从中取消(如果在do something like claim a resource 中您有类似observer.SendResult(...); 的内容,那么您可以尝试模拟observer.SendResult 并取消令牌在 n 次调用后)....没有更多代码/上下文问题是无法回答的
  • 你也许可以用一些奇怪的反射代码到达那里,但重构Run——即使你最初不想这样做——可能是最(成本)有效的方式你想要什么。

标签: c# unit-testing asynchronous rhino-mocks


【解决方案1】:

在不更改代码的情况下,您能做的最好的事情就是在特定时间后取消。 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 的新类,以跟踪 IsCancellationRequested 被测试了多少次,但这是不可能的。

【讨论】:

  • 为了安全起见等待 6 别担心....它永远不会被调用... IsCancellationRequested 是 CancellationToken 结构的属性,而不是 CancellationTokenSource 类
  • @Selvin 是的,我猜Run 在取消之前不会返回。我的错。我修好了。而且,IsCancellationRequested 是两者的属性,CancellationToken.IsCancellationRequested 只是 returns the value from CancellationTokenSource.IsCancellationRequested
【解决方案2】:

这取决于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));
    }
}

【讨论】:

    猜你喜欢
    • 2021-01-23
    • 1970-01-01
    • 2013-02-22
    • 2011-04-11
    • 2020-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多