C# 使用FromAsyncPattern进行单元测试

C# 使用FromAsyncPattern进行单元测试,c#,.net,wpf,unit-testing,system.reactive,C#,.net,Wpf,Unit Testing,System.reactive,被动扩展有一个性感的小钩子来简化异步方法的调用: var func = Observable.FromAsyncPattern<InType, OutType>( myWcfService.BeginDoStuff, myWcfService.EndDoStuff); func(inData).ObserveOnDispatcher().Subscribe(x => Foo(x)); var func=Observable.FromAsyncPattern(

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

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

func(inData).ObserveOnDispatcher().Subscribe(x => Foo(x));
var func=Observable.FromAsyncPattern(
myWcfService.begindouthing,
myWcfService.EndDoStuff);
ObserveOnDispatcher().Subscribe(x=>Foo(x));
我正在一个WPF项目中使用它,它在运行时工作得非常好

不幸的是,当尝试使用这种技术的单元测试方法时,我遇到了随机失败~包含此代码的测试每五次执行中就有三次失败

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

[TestMethod()]
公共测试()
{
//安排
var container=GetAutoMockingContainer();
container.Resolve()
.Expect(x=>x.BeginDoStuff(null,null,null))
.IgnoreArguments()
.做(
新函数((inData、异步回调、状态)=>
{
返回新的CompletedAsyncResult(asyncCallback,state);
}));
container.Resolve()
.Expect(x=>x.EndDoStuff(null))
.IgnoreArguments()
.做(
新函数((ar)=>
{
返回数据;
}));
//表演
var target=CreateTestSubject(容器);
target.domethodhattinvokeService();
//在后台优先级上运行调度程序
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background,新操作(()=>{}));
//断言
IsTrue(我的操作按预期运行);
}
我看到的问题是,我指定在异步操作完成时运行的代码(在本例中为Foo(x))从未被调用。我可以通过在Foo中设置断点并观察它们从未到达来验证这一点。此外,我可以在调用DomethodhatInvokeService(启动异步调用)后强制执行长时间延迟,代码仍然不会运行。我知道调用Rx框架的代码行被调用了

我尝试过的其他事情:

  • 我试图根据这里的建议修改最后一行:没有爱

  • 我在Rx代码中添加了
    。取(1)
    ,如下所示:

    func(inData).ObserveOnDispatcher().Take(1).Subscribe(x=>Foo(x))

这将我的失败率提高到大约1/5,但它们仍然发生

  • 我已经重写了Rx代码以使用纯jane异步模式。这是可行的,但是我的开发者ego真的很喜欢使用Rx,而不是无聊的老开始/结束
最后,我确实有一个解决办法(即不使用Rx),但我觉得这并不理想。如果有人在过去遇到过这个问题并找到了解决方案,我非常乐意听到

更新


我也很高兴,他们将在即将发布的版本中包括一个测试调度程序。一旦可用,这可能是最终的解决方案。

该问题是由
ObserveOnDispatcher
调度的调用的异步性质造成的。您不能保证在测试完成时它们都已完成。因此,您需要将日程安排置于您的控制之下

将调度程序注入到类中如何

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

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

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

公共静态MyObservableExtensions
{
公共静态ISScheduler UISafeScheduler{get;set;}
公共静态IObservable ObserveUnisafeScheduler(此IObservable源)
{
if(UISafeScheduler==null)
{
抛出新的无效操作(“UISafeScheduler尚未初始化”);
}
返回source.ObserveOn(UISafeScheduler);
}
}

然后在运行时,使用DispatchersScheduler初始化UISafeScheduler,并在测试中使用伪调度器初始化它。

问题在于MSTest.exe运行一个调度器(即Dispatcher.Current!=null),因此ObserveOnDispatcher可以工作。然而,这个调度器什么也不做!(即,队列调度程序项将被忽略)您编写的任何显式使用Schedule的代码。调度程序是不稳定的

我在年用蛮力解决了这个问题-以下是重要的一点:

我们基本上设置了一个全局变量来定义默认的调度程序,然后我们尝试在测试运行程序中进行检测


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

这看起来是最好的短期解决方案-我特别欣赏扩展方法方法的优雅..更新:
[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);
}
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);
   }
}