C# 在反应式管道中执行TPL代码并通过测试调度器控制执行

C# 在反应式管道中执行TPL代码并通过测试调度器控制执行,c#,.net,task-parallel-library,system.reactive,C#,.net,Task Parallel Library,System.reactive,我正在努力弄清楚为什么以下测试不起作用: [Fact] public void repro() { var scheduler = new TestScheduler(); var count = 0; // this observable is a simplification of the system under test // I've just included it directly in the test for clarity // in

我正在努力弄清楚为什么以下测试不起作用:

[Fact]
public void repro()
{
    var scheduler = new TestScheduler();
    var count = 0;

    // this observable is a simplification of the system under test
    // I've just included it directly in the test for clarity
    // in reality it is NOT accessible from the test code - it is
    // an implementation detail of the system under test
    // but by passing in a TestScheduler to the sut, the test code
    // can theoretically control the execution of the pipeline
    // but per this question, that doesn't work when using FromAsync
    Observable
        .Return(1)
        .Select(i => Observable.FromAsync(Whatever))
        .Concat()
        .ObserveOn(scheduler)
        .Subscribe(_ => Interlocked.Increment(ref count));

    Assert.Equal(0, count);

    // this call initiates the observable pipeline, but does not
    // wait until the entire pipeline has been executed before
    // returning control to the caller
    // the question is: why? Rx knows I'm instigating an async task
    // as part of the pipeline (that's the point of the FromAsync
    // method), so why can't it still treat the pipeline atomically
    // when I call Start() on the scheduler?
    scheduler.Start();

    // count is still zero at this point
    Assert.Equal(1, count);
}

private async Task<Unit> Whatever()
{
    await Task.Delay(100);
    return Unit.Default;
}
然后测试通过了,但这当然违背了我想要达到的目的。我可以想象在
TestScheduler
上有一个
StartAsync()
方法,我可以等待它,但它并不存在


有谁能告诉我,是否有一种方法可以让我启动反应式管道的执行,并等待它的完成,即使它包含异步调用?

nosertio编写此测试的更优雅的Rx方式。您可以
等待
可观察对象获取其最后一个值。与
Count()
结合,它就变得微不足道了

请注意,
TestScheduler
在本例中没有任何用途

[Fact]
public async Task repro()
{
    var scheduler = new TestScheduler();

    var countObs = Observable
        .Return(1)
        .Select(i => Observable.FromAsync(Whatever))
        .Concat()
        //.ObserveOn(scheduler) // serves no purpose in this test
        .Count();

    Assert.Equal(0, count);
    //scheduler.Start(); // serves no purpose in this test.

    var count = await countObs;

    Assert.Equal(1, count);
}

让我把你的问题归结为要点:

是否有一种方法,使用
TestScheduler
,执行反应式管道并等待其完成,即使它包含异步调用

我应该提前警告你,这里没有快速简单的答案,也没有可以部署的方便“技巧”

异步调用和调度程序 为了回答这个问题,我认为我们需要澄清一些要点。上述问题中的术语“异步调用”似乎专门用于指具有
任务
任务
签名的方法,即使用任务并行库(TPL)异步运行的方法

这一点很重要,因为被动扩展(Rx)采用不同的方法来处理异步操作

在Rx中,并发的引入是通过调度器来管理的,调度器是实现接口的一种类型。任何引入并发性的操作都应该使调度程序参数可用,以便调用方可以决定合适的调度程序。核心图书馆严格遵守这一原则。因此,例如,
Delay
允许指定调度程序,但
不允许指定调度程序

从中可以看到,
IScheduler
提供了大量的
Schedule
重载。需要并发的操作使用这些来计划工作的执行。工作的具体执行方式完全取决于调度器。这就是调度器抽象的威力

引入并发的Rx操作通常会提供允许忽略调度程序的重载,在这种情况下,请选择一个合理的默认值。这一点需要注意,因为如果您希望通过使用
TestScheduler
对代码进行测试,则必须对所有引入并发性的操作使用
TestScheduler
。不允许这样做的流氓方法可能会破坏您的测试工作

第三方物流调度抽象 TPL有自己的抽象来处理并发:任务调度器
TaskScheduler
。这个想法非常相似

这两种抽象之间有两个非常重要的区别:

  • Rx调度程序对自己的时间概念有一个一流的表示形式,即
    Now
    属性。第三方物流调度程序不会
  • 在TPL中使用定制调度程序的情况要少得多,并且没有提供重载的最佳实践来为引入并发性的方法提供特定的
    任务调度程序
    (返回
    任务
    任务
    )。绝大多数
    Task
    返回方法假定使用默认的
    TaskScheduler
    ,并且让您无法选择在何处运行工作
TestScheduler的动机 使用
TestScheduler
的动机通常有两个方面:

  • 通过加快时间来消除“等待”操作的需要
  • 检查事件是否在预期的时间点发生
这种工作方式完全取决于调度员有自己的时间概念这一事实。每次通过
IScheduler
调度一个操作时,我们都会指定它必须执行的时间——要么尽快执行,要么在将来的特定时间执行。然后,调度程序将工作排队等待执行,并在达到指定时间(根据调度程序本身)时执行

当您在
TestScheduler
上调用
Start
时,它的工作方式是清空所有操作的队列,执行时间等于或早于当前的
Now
——然后将其时钟提前到下一个计划工作时间并重复,直到其队列为空

这允许一些巧妙的技巧,比如能够测试一个操作永远不会导致事件!如果使用实时,这将是一项具有挑战性的任务,但是使用虚拟时间很容易-一旦调度程序队列完全为空,那么
TestScheduler
将得出结论,不再发生任何事件-因为如果队列中没有任何内容,就没有任何内容可以安排进一步的任务。实际上,
Start
正是在这一点返回的。要使其工作,显然必须在
TestScheduler
上调度所有要测量的并发操作

一个不小心做出自己的调度器选择而不允许覆盖该选择的自定义操作符,或者一个使用自己的并发形式而不考虑时间的操作(例如基于TPL的调用),将使通过
TestScheduler
控制执行变得困难(如果不是不可能的话)

如果您通过其他方式运行异步操作,那么明智地使用
TestScheduler
AdvanceTo
AdvanceBy
方法可以允许您与该外部并发源进行协调,但可实现的程度取决于该外部源提供的控制

在TPL的例子中,您确实知道任务何时完成——这允许在测试中使用等待和超时,例如
[Fact]
public async Task repro()
{
    var scheduler = new TestScheduler();

    var countObs = Observable
        .Return(1)
        .Select(i => Observable.FromAsync(Whatever))
        .Concat()
        //.ObserveOn(scheduler) // serves no purpose in this test
        .Count();

    Assert.Equal(0, count);
    //scheduler.Start(); // serves no purpose in this test.

    var count = await countObs;

    Assert.Equal(1, count);
}
public static IObservable<T> ToTestScheduledObseravble<T>(
    this Task<T> task,
    TestScheduler scheduler,
    TimeSpan duration,
    TimeSpan? timeout = null)
{   

    timeout = timeout ?? TimeSpan.FromSeconds(100);
    var subject = Subject.Synchronize(new AsyncSubject<T>(), scheduler);              

    scheduler.Schedule<Task<T>>(task, duration,
        (s, t) => {
            if (!task.Wait(timeout.Value))
            {           
                subject.OnError(
                    new TimeoutException(
                    "Task duration too long"));                        
            }
            else
            {
                switch (task.Status)
                {
                    case TaskStatus.RanToCompletion:
                        subject.OnNext(task.Result);
                        subject.OnCompleted();
                        break;
                    case TaskStatus.Faulted:
                        subject.OnError(task.Exception.InnerException);
                        break;
                    case TaskStatus.Canceled:
                        subject.OnError(new TaskCanceledException(task));
                        break;
                }
            }

            return Disposable.Empty;
        });

    return subject.AsObservable();
}
Observable
    .Return(1)
    .Select(i => Whatever().ToTestScheduledObseravble(
        scheduler, TimeSpan.FromSeconds(1)))
    .Concat()
    .Subscribe(_ => Interlocked.Increment(ref count));
[Fact]
public void repro()
{
    var scheduler = new TestScheduler();
    var count = 0;

    // this observable is a simplification of the system under test
    // I've just included it directly in the test for clarity
    // in reality it is NOT accessible from the test code - it is
    // an implementation detail of the system under test
    // but by passing in a TestScheduler to the sut, the test code
    // can theoretically control the execution of the pipeline
    // but per this question, that doesn't work when using FromAsync
    Observable
        .Return(1)
        .Select(_ => Observable.FromAsync(()=>Whatever(scheduler)))
        .Concat()
        .ObserveOn(scheduler)
        .Subscribe(_ => Interlocked.Increment(ref count));

    Assert.Equal(0, count);

    // this call initiates the observable pipeline, but does not
    // wait until the entire pipeline has been executed before
    // returning control to the caller
    // the question is: why? Rx knows I'm instigating an async task
    // as part of the pipeline (that's the point of the FromAsync
    // method), so why can't it still treat the pipeline atomically
    // when I call Start() on the scheduler?
    scheduler.Start();

    // count is still zero at this point
    Assert.Equal(1, count);
}

private async Task<Unit> Whatever(IScheduler scheduler)
{
    return await Observable.Timer(TimeSpan.FromMilliseconds(100), scheduler).Select(_=>Unit.Default).ToTask();
}