如何对包含异步调用的方法进行单元测试?

5
我有一个包含异步调用的方法,类似于这样:
public void MyMethod() {
    ...
    (new Action<string>(worker.DoWork)).BeginInvoke(myString, null, null);
    ...
}

我正在使用Unity,创建模拟对象并不是问题,但如何测试DoWork是否被调用而不必担心竞争条件呢? 先前的问题提供了一个解决方案,但在我看来,等待处理程序是一种hack(竞争条件仍然存在,尽管几乎不可能出现)。

编辑:好的,我原以为可以用一种非常普遍的方式提出这个问题,但看来我需要进一步阐述问题:

我想为上述的MyMethod创建一个测试,所以我会做类似于这样的事情:

[TestMethod]
public void TestMyMethod() {
   ...setup...
   MockWorker worker = new MockWorker();
   MyObject myObj = new MyObject(worker);
   ...assert preconditions...
   myObj.MyMethod();
   ...assert postconditions...
}

一种天真的方法是创建一个MockWorker(),当DoWork被调用时仅设置一个标志,并在后置条件中测试该标志。这当然会导致竞态条件,即在MockWorker中设置标志之前就检查了后置条件。

更正确的方法(我可能最终会使用)是使用等待句柄:

class MockWorker : Worker {
    public AutoResetEvent AutoResetEvent = new AutoResetEvent();

    public override void DoWork(string arg) {
        AutoResetEvent.Set();
    }
}

...并使用以下断言:

Assert.IsTrue(worker.AutoResetEvent.WaitOne(1000, false));

这是采用类似信号量的方法,这很好...但是在理论上,以下情况可能发生:

  1. 在我的DoWork委托上调用BeginInvoke。
  2. 由于某种原因,主线程或DoWork线程都没有被分配执行时间1000ms。
  3. 主线程被分配执行时间,并且由于超时而导致断言失败,即使DoWork线程尚未被执行。

我是否误解了AutoResetEvent的工作方式?我是不是太过紧张,还是有一个聪明的解决方案来解决这个问题?


你能解释一下那种情况下的竞态条件吗?那基本上是我使用的解决方案,从未让我失望过。 - Simon Steele
不,我确定它从不会失败,因为竞争条件主要是理论上的。竞态条件很简单:如果由于某种原因工作线程在超时之前从未获得执行时间,则测试将错误地失败。我知道!这纯粹是猜测,但我知道墨菲定律;-) - toxvaerd
我看到过使用这些标志的测试在负载较重的构建服务器上失败(需要重新构建,增加了负荷)。采用信号路线并使用超时对我来说似乎更好。同时,使用标志意味着您必须在某个时间段的整个时间内都处于睡眠状态,这会使得测试套件运行的时间比必要的时间长很多。 - Drew Noakes
3个回答

3
等待句柄(Wait handle)是我会使用的方法。据我所知(但不确定),异步方法本质上也是使用等待句柄来触发该方法的。
我不确定您为什么认为会出现竞争条件,除非您在WaitOne调用上给出了异常短的时间。我建议将WaitOne设置为4-5秒钟,这样您就可以确保它是否存在问题,并且这不仅仅是一场竞赛。
此外,不要忘记等待句柄的工作原理,只要等待句柄被创建,您就可以按以下顺序执行:
- 线程1 - 创建等待句柄 - 线程1 - 设置等待句柄 - 线程2 - 在等待句柄上等待 - 线程2 - 通过等待句柄并继续执行
即使正常执行顺序如下:
- 线程1 - 创建等待句柄 - 线程2 - 在等待句柄上等待 - 线程1 - 设置等待句柄 - 线程2 - 通过等待句柄并继续执行
两种方式都能正确处理,等待句柄可以在Thread2开始等待之前设置,所有事情都会为您处理好。

你能详细说明一下吗?我知道竞态条件高度理论化,但它似乎就是那种在我们发货的那天会出问题的东西;-) - toxvaerd

3

在测试普通委托时,只需使用EndInvoke调用确保委托被调用并返回适当的值。例如。

var del = new Action<string>(worker.DoWork);
var async = del.BeginInvoke(myString,null,null);
...
var result = del.EndInvoke(async);

编辑

OP表示他们正在尝试对MyMethod与worker.DoWork方法进行单元测试。

在这种情况下,您将不得不依赖于调用DoWork方法时产生的可见副作用。基于您的示例,我无法提供太多帮助,因为没有暴露DoWork的任何内部工作方式。

编辑2

[OP]由于某种原因,主线程和DoWork线程都没有被分配1000ms的执行时间。

这是不可能发生的。当您在AutoResetEvent上调用WaitOne时,线程会进入睡眠状态。除非事件设置或超时期满,否则它将不会接收到处理器时间。其他一些线程获得了大量时间片并导致虚假故障是有可能的。但我认为这相当不可能。我有几个以相同方式运行的测试,我没有像这样的虚假失败。我通常选择约2分钟的超时时间。


我不是测试委托方法本身 - 我正在测试调用委托的方法。 - toxvaerd
由于我正在使用模拟对象,因此我可以自己选择DoWork()的副作用...正如我所说 - 我正在对MyMethod进行单元测试,而不是DoWork,因此DoWork的内部工作应该是无关紧要的。 - toxvaerd
@Toxvaerd,你需要让你的问题更加清晰明了。从你的帖子中我们只能看出你有一个名为MyMethod的方法,它异步调用了一个委托,而你想知道它是否被调用。如果没有更多的细节,我们无法提供帮助。 - JaredPar
感谢您的回答。我知道这种情况很不可能发生,但我只是想知道是否有解决方案可以消除最后一点不确定性。但是,这只是一个单元测试。也许我应该放松一下,然后去做它;-)其他帖子的回答更早,所以他将获得勾选标记。 - toxvaerd

2

在这些情况下,我所做的是:创建一个服务,接收代理并执行它(通过简单的接口公开此功能,在异步调用时注入)。

在实际服务中,它将使用BeginInvoke进行异步执行。然后创建一个版本的服务进行测试,该版本同步地调用代理。

以下是示例:

public interface IActionRunner
{
   void Run(Action action, AsyncCallback callback, object obj);
   void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj);
   void Run<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2, AsyncCallback callback, object obj);
   void Run<T1, T2, T3>(Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3, AsyncCallback callback, object obj);
   void Run<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, AsyncCallback callback, object obj);
}

这项服务的异步实现如下所示:
public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj)
{
   action.BeginInvoke(arg, callback, obj);
}

这项服务的同步实现如下所示:
public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj)
{
   action(arg);
}

当你进行单元测试时,如果你使用像RhinoMocks和AutoMocking容器这样的工具,你可以替换同步ActionRunner:

_mocks = new MockRepository();

_container = new AutoMockingContainer(_mocks);
_container.AddService(typeof(IActionRunner), new SyncActionRunner());
_container.Initialize();

ActionRunner不需要进行测试,它只是方法调用的一个薄层包装。

1
这难道不会只是转移问题吗?当我想要测试服务时会发生什么? - toxvaerd
我个人不太担心测试 .Net 的功能。如果你明白我的意思,我有点期望异步调用能够直接运行。测试 BeginInvoke() 对我来说似乎不是非常有用的测试。 - Chris Holmes
服务只是一个薄的包装层;我将编辑我的帖子,以便您可以看到。 - Chris Holmes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接