2

反应式扩展有一个性感的小钩子来简化调用异步方法:

var func = Observable.FromAsyncPattern<InType, OutType>(
    myWcfService.BeginDoStuff,
    myWcfService.EndDoStuff);

func(inData).ObserveOnDispatcher().Subscribe(x => Foo(x));

我在 WPF 项目中使用它,它在运行时效果很好。

不幸的是,在尝试对使用这种技术的方法进行单元测试时,我遇到了随机失败。包含此代码的测试的每五次执行中约有 3 次失败。

这是一个示例测试(使用 Rhino/unity 自动模拟容器实现):

[TestMethod()]
public void SomeTest()
{
   // arrange
   var container = GetAutoMockingContainer();

   container.Resolve<IMyWcfServiceClient>()
      .Expect(x => x.BeginDoStuff(null, null, null))
      .IgnoreArguments()
      .Do(
         new Func<Specification, AsyncCallback, object, IAsyncResult>((inData, asyncCallback, state) =>
            {
               return new CompletedAsyncResult(asyncCallback, state);
             }));

   container.Resolve<IRepositoryServiceClient>()
      .Expect(x => x.EndDoStuff(null))
      .IgnoreArguments()
      .Do(
         new Func<IAsyncResult, OutData>((ar) =>
         {
            return someMockData;
         }));

   // act
   var target = CreateTestSubject(container);

   target.DoMethodThatInvokesService();

   // Run the dispatcher for everything over background priority
   Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new Action(() => { }));

   // assert
   Assert.IsTrue(my operation ran as expected);
}

我看到的问题是,我指定在异步操作完成时运行的代码(在本例中为 Foo(x))从未被调用。我可以通过在 Foo 中设置断点并观察它们永远不会到达来验证这一点。此外,我可以在调用 DoMethodThatInvokesService (启动异步调用)后强制长时间延迟,并且代码仍然永远不会运行。我知道调用 Rx 框架的代码行被调用了。

我尝试过的其他事情:

这将我的失败率提高到五分之一,但它们仍然发生。

  • 我已经重写了 Rx 代码以使用普通的 jane Async 模式。这行得通,但是我的开发者自我真的很想使用 Rx 而不是无聊的旧开始/结束。

最后,我确实有一个解决方法(即不要使用 Rx),但是我觉得它并不理想。如果有人过去遇到过这个问题并找到了解决方案,我非常乐意听到它。

更新

我还在Rx 论坛上发帖,他们将在即将发布的版本中包含一个测试调度程序。一旦可用,这可能是最终的解决方案。

4

2 回答 2

6

问题是 MSTest.exe 运行一个 Dispatcher(即 Dispatcher.Current != null),所以 ObserveOnDispatcher 可以工作。但是,这个 Dispatcher 什么都不做!(即排队的调度程序项目将被忽略)您编写的任何明确使用 Schedule.Dispatcher 的代码都是不可测试的

我通过ReactiveUI中的蛮力解决了这个问题- 这是重要的部分:

https://github.com/reactiveui/ReactiveUI/blob/master/ReactiveUI/RxApp.cs#L99

我们基本上设置了一个定义默认调度程序的全局变量,然后我们尝试检测我们何时处于测试运行器中。

然后,在我们实现 IObservable 的类中,都采用 IScheduler 参数,其默认值最终将成为全局默认调度程序。我本可以做得更好,但这对我有用,并使 ViewModel 代码再次可测试。

于 2010-07-09T05:18:30.823 回答
3

该问题是由ObserveOnDispatcher. 您不能保证在测试完成时它们都已完成。所以你需要把日程安排在你的控制之下。

将调度程序注入您的班级怎么样?

然后,不是调用ObserveOnDispatcher,而是调用ObserveOn,传入IScheduler注入的实现。

在运行时,您将注入DispatcherScheduler,但在您的测试中,您将注入一个虚假的调度程序,该调度程序将给定的所有操作排队并在您的测试控制的时间运行它们。

如果您不喜欢在使用 Rx 的任何地方都必须注入调度程序的想法,那么创建自己的扩展方法怎么样,像这样(前面的未经测试的代码):

public static MyObservableExtensions
{
   public static IScheduler UISafeScheduler {get;set;}

   public static IObservable<TSource> ObserveOnUISafeScheduler(this IObservable<TSource> source)
   {
       if (UISafeScheduler == null) 
       {
          throw new InvalidOperation("UISafeScheduler has not been initialised");
       }

       return source.ObserveOn(UISafeScheduler);
   }
}

然后在运行时,用 DispatcherScheduler 初始化 UISafeScheduler,并在你的测试中,用你的假调度器初始化它。

于 2010-06-11T10:12:37.657 回答