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

时间:2009-02-18 14:58:13

标签: c# unit-testing concurrency

我有一个包含这样的异步调用的方法:

public void MyMethod() {
    ...
    (new Action<string>(worker.DoWork)).BeginInvoke(myString, null, null);
    ...
}

我正在使用Unity并且创建模拟对象不是问题,但是如何在不担心竞争条件的情况下测试DoWork的调用?

A previous question提供了一个解决方案,但在我看来,等待处理是一个黑客攻击(竞争条件仍然存在,虽然它几乎不可能提升)。


编辑:好的,我想我可以用一般的方式提出这个问题,但似乎我必须进一步详细说明这个问题:

我想为上面提到的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代理
  2. 上调用BeginInvoke
  3. 由于某种原因,主线程或DoWork线程都没有给出1000ms的执行时间。
  4. 主线程被赋予执行时间,并且由于超时,断言失败,即使DoWork线程尚未执行。
  5. 我是否误解了AutoResetEvent的工作原理?我只是太偏执了,还是有解决这个问题的聪明方法?

3 个答案:

答案 0 :(得分:3)

等待处理将是我的方式。 AFAIK(我当然不知道)异步方法正在使用等待句柄来触发该方法。

我不确定为什么你会认为比赛条件会发生,除非你在WaitOne电话上给出了异常短的时间。我会在等待时间上放4-5秒,这样你就可以确定它是否被打破了,这不仅仅是一场比赛。

另外,不要忘记等待句柄是如何工作的,只要创建了等待句柄,就可以执行以下执行顺序

  • 主题1 - 创建等待句柄
  • 主题1 - 设置等待句柄
  • 线程2 - 等待句柄上的waitone
  • 线程2 - 吹过等待句柄并继续执行

即使正常执行

  • 主题1 - 创建等待句柄
  • 线程2 - 等待句柄上的waitone
  • 主题1 - 设置等待句柄
  • 线程2 - 吹过等待句柄并继续执行

要么正常工作,可以在Thread2开始等待之前设置等待句柄,然后为你处理所有事情。

答案 1 :(得分: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的内部工作都没有暴露出来。

<强> EDIT2

[OP]由于某种原因,主线程或DoWork线程都没有给出1000ms的执行时间。

这不会发生。当您在AutoResetEvent上调用WaitOne时,线程将进入休眠状态。在事件设置或超时期限到期之前,它将不会收到任何处理器时间。一些其他线程获得重要的时间片并导致错误的失败,这绝对是可行的。但我认为这是不太可能的。我有几个测试以相同的方式运行,我没有得到很多这样的错误失败。我通常选择超时约2分钟。

答案 2 :(得分: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);
}

此服务的Asycnhronous实现如下所示:

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容器之类的东西,则可以交换Synchronous ActionRunner:

_mocks = new MockRepository();

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

不需要测试ActionRunner;它只是方法调用的一个薄薄的贴面。