2010-01-21 83 views
19

在下面的代码示例中,我有一个异步计算器类。这是注入ICalc,这将是一个同步计算器。我使用依赖注入和模拟ICalc,因为这类似于我的真实场景,但我猜测这个嘲讽与这个问题并不相关。 AsyncCalc有一个函数,它将异步调用另一个函数 - 以回调为参数。当异步函数调用完成时,回调将被触发并返回结果。单元测试异步函数

现在我想测试我的异步函数 - 检查回调是否触发了期望的参数。此代码似乎工作。但是,我觉得随时都可能会崩溃 - 我担心的是在函数结束和测试结束之前完成回调的竞态条件 - 因为这将在单独的线程中运行。

我现在的问题是,如果我在正确的轨道单元测试异步功能,或者如果有人可以帮助我走上正确的轨道..?如果我可以确保回调立即触发 - 最好是在我猜测的同一线索上,会更好吗?可以/应该完成吗?

public interface ICalc 
{ 
    int AddNumbers(int a, int b); 
} 

public class AsyncCalc 
{ 
    private readonly ICalc _calc; 
    public delegate void ResultProcessor(int result); 
    public delegate int AddNumbersAsyncCaller(int a, int b); 

    public AsyncCalc(ICalc calc) 
    { 
     _calc = calc; 
    } 

    public void AddNumbers(int a, int b, ResultProcessor resultProcessor) 
    { 
     var caller = new AddNumbersAsyncCaller(_calc.AddNumbers); 
     caller.BeginInvoke(a, b, new AsyncCallback(AddNumbersCallbackMethod), resultProcessor); 
    } 

    public void AddNumbersCallbackMethod(IAsyncResult ar) 
    { 
     var result = (AsyncResult)ar; 
     var caller = (AddNumbersAsyncCaller)result.AsyncDelegate; 
     var resultFromAdd = caller.EndInvoke(ar); 

     var resultProcessor = ar.AsyncState as ResultProcessor; 
     if (resultProcessor == null) return; 

     resultProcessor(resultFromAdd); 
    }    
} 

[Test] 
public void TestingAsyncCalc() 
{ 
    var mocks = new MockRepository(); 
    var fakeCalc = mocks.DynamicMock<ICalc>(); 

    using (mocks.Record()) 
    { 
     fakeCalc.AddNumbers(1, 2); 
     LastCall.Return(3); 
    } 

    var asyncCalc = new AsyncCalc(fakeCalc); 
    asyncCalc.AddNumbers(1, 2, TestResultProcessor); 
} 

public void TestResultProcessor(int result) 
{ 
    Assert.AreEqual(3, result); 
} 
+1

我觉得你的测试只需要等待一个循环,直到执行回调。 –

回答

20

您可以使用ManualResetEvent来同步您的线程。

在以下示例中,测试线程将阻止对completion.WaitOne()的调用。 异步计算的回调存储结果,,然后通过调用completion.Set()来通知事件。

[Test] 
public void TestingAsyncCalc() 
{ 
    var mocks = new MockRepository(); 
    var fakeCalc = mocks.DynamicMock<ICalc>(); 

    using (mocks.Record()) 
    { 
     fakeCalc.AddNumbers(1, 2); 
     LastCall.Return(3); 
    } 

    var asyncCalc = new AsyncCalc(fakeCalc); 

    var completion = new ManualResetEvent(false); 
    int result = 0; 
    asyncCalc.AddNumbers(1, 2, r => { result = r; completion.Set(); }); 
    completion.WaitOne(); 

    Assert.AreEqual(3, calcResult); 
} 

// ** USING AN ANONYMOUS METHOD INSTEAD 
// public void TestResultProcessor(int result) 
// { 
//  Assert.AreEqual(3, result); 
// } 
+0

Freakin真棒!完美的作品。正是我需要的。谢谢! – stiank81

+3

我只会补充说,对于某些场景(外部服务),您可能需要添加一个超时:Assert.IsTrue(completion.WaitOne(TimeSpan.FromSeconds(10)),“Calculation completed”); – Rashack

0

您也可以使用“测试运行器”类来在循环中运行断言。循环会在try/catch中运行断言。异常处理程序只会尝试再次运行断言,直到超时过期。我最近写了一篇关于这种技术的博客文章。这个例子在groovy中,但适用于任何语言。而不是传递闭包,你可以在c#中传递一个Action。

http://www.greenmoonsoftware.com/2013/08/asynchronous-functional-testing/