.NET Rx Koans:为什么这个测试用例失败了?

时间:2013-03-07 20:16:34

标签: .net unit-testing asynchronous system.reactive

在寻找学习Rx的材料时,我发现了这一点:Reactive Extensions (Rx) Koans。来自介绍:

  

'Koan'的定义
  Kōans是一个禅词,意思是一个人的启蒙或觉醒,通常是通过拼图或谜语。最常见的是“单手拍手的声音是什么?”

它由许多简短的测试用例组成,它们教授Rx的不同方面。

其中一个应该直观地通过,然而,它失败了。你能解释一下原因吗?

完全在这里:

    [TestMethod]
    [Timeout(___)] //"Fill in the blanks" - I tried several values, e.g. 4000. No changes.
    public void AsynchronousRunInParallel()
    {
        Func<int, int> inc = (int x) =>
                                {
                                    // I set a breakpoint here and it's never hit.

                                    Thread.Sleep(1500);
                                    return x + 1;
                                };
        double result = 0;
        var incAsync = Observable.FromAsyncPattern<int, int>(inc.BeginInvoke,
                                                             inc.EndInvoke);
        incAsync(1).Merge(incAsync(9)).Sum()
                               .SubscribeOn(Scheduler.Immediate)
                               .Subscribe(n => result = n);

        Assert.AreEqual(12, result);
                    //the failing message says: 'expected 12, got 0'
    }

3 个答案:

答案 0 :(得分:4)

简短回答:

在检查结果之前,你没有足够的时间让测试的异步部分执行。

更长的答案:

此测试执行的操作顺序有点像:

  • 设置将{异步调用委托
  • IObservable
  • 将其链接到另一个IObservable,这是两个异步调用的合并结果,汇总在一起
  • Subscribe生成的IObservable,导致方法的两次异步调用
  • 卫生署!委托中有Thread.Sleep,因此异步调用被阻止!
  • 立即检查结果,当然是0 - 两个被阻止的异步调用没有“完成”

您可以通过多种方式“修复”此问题:

  • 删除Thread.Sleep
  • 通过更改BeginInvoke将呼叫更改为同步,但这需要对整个测试进行重组
  • 使用HistoricalScheduler代替Immediate一个

在尝试单元测试Rx时,强烈建议使用HistoricalScheduler - 基本上,它允许您在虚拟时间内前后跳转,这是测试与时间相关的代码(如Rx查询)的关键功能:

var theTardis = new HistoricalScheduler();

Func<int, int> inc = (int x) =>
{
    theTardis.Sleep(TimeSpan.FromMilliseconds(1500));
    return x + 1;
};
double result = 0;
var incAsync = Observable.FromAsyncPattern<int, int>(inc.BeginInvoke,inc.EndInvoke);

incAsync(1).Merge(incAsync(9)).Sum()
    .SubscribeOn(theTardis)
    .Subscribe(n => result = n);

// To the FUTURE!
theTardis.AdvanceBy(TimeSpan.FromSeconds(5));

Assert.AreEqual(12, result);

答案 1 :(得分:3)

这是同步版本的样子 - 你所拥有的最直接的版本。 Single()将阻塞,直到observable完成。阻止通常是你想要避免的,但如果你只是搞乱它就没问题。

public void AsynchronousRunInParallel()
{
    Func<int, int> inc = (int x) =>
    {
        Thread.Sleep(1500);
        return x + 1;
    };

    var incAsync = Observable.FromAsyncPattern<int, int>(inc.BeginInvoke,
                                                         inc.EndInvoke);

    int sum = incAsync(1).Merge(incAsync(9)).Sum().Single();

    Assert.AreEqual(12, sum);
}

使用await

的异步TPL版本
public async Task AsynchronousRunInParallel()
{
    Func<int, int> inc = (int x) =>
    {
        Thread.Sleep(1500);
        return x + 1;
    };

    var incAsync = Observable.FromAsyncPattern<int, int>(inc.BeginInvoke,
                                                         inc.EndInvoke);

    int sum = await incAsync(1).Merge(incAsync(9)).Sum();

    Assert.AreEqual(12, sum);
}

最后是一个使用Rx Do()的异步 - 如果说这是一个更大的操作的一部分就好了:

public async Task AsynchronousRunInParallel()
{
    Func<int, int> inc = (int x) =>
    {
        Thread.Sleep(1500);
        return x + 1;
    };

    var incAsync = Observable.FromAsyncPattern<int, int>(inc.BeginInvoke,
                                                         inc.EndInvoke);

    await incAsync(1).Merge(incAsync(9)).Sum().Do(sum =>
    {
        Assert.AreEqual(12, sum);
    });
}

答案 2 :(得分:2)

在我看来,它是源代码中的一个错误,AsynchronousRunInParallel缺少等待的结果,例如在TheBloodyHardAsyncInvokationPatter中使用。

使用FromAsyncPattern时,只在当前线程中同步执行BeginInvoke。将在ThreadPool上安排实际工作和结果处理。因此,只要安排了2个异步增量,就会立即执行Assert,而无需等待完成。

我添加了:

ThreadUtils.WaitUntil(() => result != 0.0);

所以结果如下:

        incAsync(1).Merge(incAsync(9)).
            Sum().SubscribeOn(Scheduler.Immediate).
            Subscribe(n => result = n);
        ThreadUtils.WaitUntil(() => result != 0.0);
        Assert.AreEqual(12, result);

或者您可能希望将“订阅”替换为“运行”。它是Koan本身的辅助方法,它将使用手动事件等待:

incAsync(1).Merge(incAsync(9)).
             Sum().SubscribeOn(Scheduler.Immediate).
             Run(n => result = n);
Assert.AreEqual(12, result);