异步函数的单元测试

21
在下面的代码示例中,我有一个异步计算器类Async Calculator。它被注入了一个同步计算器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
我认为你的测试只需要在循环中等待回调执行即可。 - Robert Harvey
2个回答

23
你可以使用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);
// }

3
我只想补充一下,在某些情况下(例如外部服务),你可能需要添加一个超时时间:Assert.IsTrue(completion.WaitOne(TimeSpan.FromSeconds(10)), "Calculation completed"); - Rashack

0
你也可以使用一个“测试运行器”类来循环运行断言。循环将在try/catch中运行断言。异常处理程序将简单地尝试再次运行断言,直到超时已过期。我最近写了一篇关于这种技术的博客文章。示例是用Groovy编写的,但适用于任何语言。在C#中,你可以传递一个Action而不是一个Closure。

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


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