在长时间运行的单元测试中协调取消

时间:2010-06-08 13:23:44

标签: unit-testing asynchronous mocking

我正在为我的应用程序的“粘合”层编写单元测试,并且很难为异步方法创建确定性测试,允许用户过早地取消操作。

具体来说,在一些异步方法中,我们有代码响应取消调用并确保对象在完成之前处于正确状态。我想确保测试涵盖这些代码路径。

在此场景中举例说明典型异步方法的一些C#伪代码如下:

public void FooAsync(CancellationToken token, Action<FooCompletedEventArgs> callback) 
{
   if (token.IsCancellationRequested) DoSomeCleanup0();

   // Call the four helper methods, checking for cancellations in between each
   Exception encounteredException;
   try
   {
      MyDependency.DoExpensiveStuff1();
      if (token.IsCancellationRequested) DoSomeCleanup1();

      MyDependency.DoExpensiveStuff2();
      if (token.IsCancellationRequested) DoSomeCleanup2();

      MyDependency.DoExpensiveStuff3();
      if (token.IsCancellationRequested) DoSomeCleanup3();

      MyDependency.DoExpensiveStuff4();
      if (token.IsCancellationRequested) DoSomeCleanup4();

   }
   catch (Exception e)
   {
      encounteredException = e;
   }

   if (!token.IsCancellationRequested)
   {
      var args = new FooCompletedEventArgs(a bunch of params);
      callback(args);
   }
}

到目前为止,我提出的解决方案涉及模拟由粘合层包装的基础MyDependency操作,并强制每个操作一段任意时间。然后我调用异步方法,并在取消异步请求之前告诉我的单元测试要休眠几毫秒。

像这样的东西(以Rhino Mocks为例):

[TestMethod]
public void FooAsyncTest_CancelAfter2()
{
   // arrange
   var myDependency = MockRepository.GenerateStub<IMyDependency>();

   // Set these stubs up to take a little bit of time each so we can orcestrate the cancels
   myDependency.Stub(x => x.DoExpensiveStuff1()).WhenCalled(x => Thread.Sleep(100));
   myDependency.Stub(x => x.DoExpensiveStuff2()).WhenCalled(x => Thread.Sleep(100));
   myDependency.Stub(x => x.DoExpensiveStuff3()).WhenCalled(x => Thread.Sleep(100));
   myDependency.Stub(x => x.DoExpensiveStuff4()).WhenCalled(x => Thread.Sleep(100));

   // act
   var target = new FooClass(myDependency);

   CancellationTokenSource cts = new CancellationTokenSource();
   bool wasCancelled = false;

   target.FooAsync(
      cts.Token,
      args =>
      {
         wasCancelled = args.IsCancelled;
         // Some other code to manipulate FooCompletedEventArgs
      });

   // sleep long enough for two operations to complete, then cancel
   Thread.Sleep(250);
   cts.Cancel();

   // Some code to ensure the async call completes goes here

   //assert
   Assert.IsTrue(wasCancelled);
   // Other assertions to validate state of target go here
}

除了在单元测试中使用Thread.Sleep让我感到不安的事实之外,更大的问题是,如果碰巧在重负载下,有时这样的测试会在我们的构建服务器上失败。异步调用太过分了,取消来得太晚了。

任何人都可以为这样的长时间运行提供更可靠的单元测试取消逻辑方式吗?任何想法都将不胜感激。

2 个答案:

答案 0 :(得分:6)

我会尝试使用模拟以同步方式“模拟”异步行为。而不是使用

myDependency.Stub(x => x.DoExpensiveStuff1()).WhenCalled(x => Thread.Sleep(100));

然后将取消标志设置在任何毫秒数内,我只是将其设置为回调的一部分:

myDependency.Stub(x => x.DoExpensiveStuff1());
myDependency.Stub(x => x.DoExpensiveStuff2());
myDependency.Stub(x => x.DoExpensiveStuff3()).WhenCalled(x => cts.Cancel());
myDependency.Stub(x => x.DoExpensiveStuff4());

从您的代码的角度来看,这看起来好像在通话期间发生了取消。

答案 1 :(得分:1)

每个长时间运行的操作都应该在事件开始运行时触发。

在单元测试中将此事件挂起。这给出了确定性结果,以及事件在将来可能有用的潜力。