如何对包含异步调用的方法进行单元测试?
我有一个包含这样的异步调用的方法:如何对包含异步调用的方法进行单元测试?
public void MyMethod() {
...
(new Action<string>(worker.DoWork)).BeginInvoke(myString, null, null);
...
}
我使用的是统一和创建模拟对象是没有问题的,但我怎么能测试的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中设置标志之前检查后置条件。
更正确的做法(这我可能会最终使用)使用等待句柄:
...并使用以下断言:
Assert.IsTrue(worker.AutoResetEvent.WaitOne(1000, false));
这是这很好......但在理论下面可能会发生:
- 在我的DoWork代理上调用BeginInvoke
- 由于某些原因,主线程或DoWork线程的执行时间均为1000毫秒。
- 主线程被赋予执行时间,并且由于超时断言失败,即使DoWork线程尚未执行。
我误解了AutoResetEvent的工作原理吗?我只是太偏执,还是有一个聪明的解决方案来解决这个问题?
等待处理将是我会这样做的方式。 AFAIK(我不知道肯定)异步方法正在使用等待句柄来触发该方法。
我不确定为什么你会认为竞争情况会发生,除非你在WaitOne调用中给出异常短的时间。我会等待4-5秒,这样你肯定知道它是否被破坏,而不仅仅是一场比赛。
另外,不要忘记如何等待句柄的工作,只要在创建等待句柄,您可以执行下列顺序
- 线程1 - 创建等待处理
- 线程1 - 设置等候处理
- 线程2 - 等待句柄的WaitOne
- 线程2 - 通过等待句柄打击,并继续执行
即使正常执行是
- 线程1 - 创建等待处理
- 线程2 - 的WaitOne在等待句柄
- 线程1 - 集等待处理
- 线程2 - 通过等待句柄打击并继续执行
可以正常工作,可以在Thread2开始等待它之前设置等待句柄,并且为您处理所有事情。
你能详细解释一下吗?我知道比赛条件是非常理论的,但它似乎是我们出货的那一天会出错的;-) – toxvaerd 2009-02-18 15:28:34
当测试直接委托时,只需使用EndInvoke调用来确保委托被调用并返回相应的值。例如。
var del = new Action<string>(worker.DoWork);
var async = del.BeginInvoke(myString,null,null);
...
var result = del.EndInvoke(async);
EDIT
OP评论说,他们正试图单元测试的MyMethod相对于worker.DoWork方法。
在这种情况下,您将不得不依靠调用DoWork方法的可见副作用。根据你的例子,我不能在这里提供太多内容,因为DoWork的内部工作都没有公开。
EDIT2
[OP]出于某种原因,既不是主线程或DoWork的线程,给出了1000毫秒执行时间。
这不会发生。当您在AutoResetEvent上调用WaitOne时,线程会进入休眠状态。直到事件设置或超时时间到期,它才会收到处理器时间。一些其他线程获得大量时间片并导致错误失败是绝对可以的。但我认为这是不太可能的。我有几个以相同方式运行的测试,并且我没有很多/任何这样的错误失败。我通常选择一个超时〜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和AutoMoc之类的东西你可以在你的Synchronous ActionRunner中进行交换:
_mocks = new MockRepository();
_container = new AutoMockingContainer(_mocks);
_container.AddService(typeof(IActionRunner), new SyncActionRunner());
_container.Initialize();
ActionRunner不需要测试;它只是方法调用的薄木板。
你能解释一下这种情况下的比赛状况吗?这几乎是我使用的解决方案,它永远不会让我失望。 – 2009-02-18 15:05:10
不,我相信它永远不会失败,因为竞争条件主要是理论上的。 竞争条件很简单:如果由于某种原因,工作线程在超时之前从未被赋予执行时间,则测试错误地失败。 我知道!这是猜测性的,但我知道墨菲定律;-) – toxvaerd 2009-02-18 15:26:10
我见过使用这些标志的测试会在重负载的构建服务器上失败(需要重建以增加负载)。走信号路线并使用暂停对我来说似乎更好。另外,使用标志意味着你必须在整个一段时间内进入睡眠状态,这可能会让你的测试套件花费很多时间才能运行。 – 2009-05-09 19:32:04