代码之家  ›  专栏  ›  技术社区  ›  toxvaerd

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

  •  4
  • toxvaerd  · 技术社区  · 16 年前

    我有一个包含如下异步调用的方法:

    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...
    }
    

    nave的方法是创建一个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线程尚未执行。

    我是否误解了AutoResetEvent的工作原理?是我太偏执了,还是有一个聪明的解决办法?

    3 回复  |  直到 7 年前
        1
  •  3
  •   Allen Rice 0x6A75616E    16 年前

    等等,我会这么做的。AFAIK(我不确定)异步方法使用等待句柄来触发该方法。

    我不知道你为什么认为比赛条件会发生,除非你在WaitOne通话中给出了异常短的时间。我会花4-5秒在waitone上,这样你就可以确定它是否坏了,这不仅仅是一场比赛。

    • 线程1-创建等待句柄
    • 线程2-等待句柄上的waitone
    • 线程2-遍历等待句柄并继续执行

    • 线程1-创建等待句柄
    • 线程2-等待句柄上的waitone
    • 线程1-设置等待句柄
    • 线程2-遍历等待句柄并继续执行

    这两种方法都可以正常工作,可以在Thread2开始等待之前设置等待句柄,并为您处理所有事情。

        2
  •  3
  •   JaredPar    16 年前

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

    编辑

    OP评论说,他们正在尝试对MyMethod和worker.DoWork方法进行单元测试。

    在这种情况下,您将不得不依赖于调用DoWork方法的可见副作用。根据你的例子,我在这里不能提供太多,因为没有一个道工的内部工作是暴露出来的。

    编辑2

    [OP]由于某些原因,主线程或定位销线程的执行时间都不超过1000毫秒。

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

        3
  •  2
  •   Chris Holmes    15 年前

    在实际服务中,它将使用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);
    }
    

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

    ActionRunner不需要测试;这只是方法调用上的一层薄薄的外衣。