Модульное тестирование с помощью FromAsyncPattern

У Reactive Extensions есть небольшая ловушка для упрощения вызова асинхронных методов:

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.

Другие вещи, которые я пробовал:

Это увеличило количество моих неудач примерно до 1 из 5, но они все равно имели место.

  • Я переписал код Rx, чтобы использовать простой шаблон Jane Async. Это работает, однако мое эго разработчика действительно хотело бы использовать Rx вместо утомительного старого begin / end.

В конце концов, у меня есть работа (т.е. не использую Rx), но я считаю, что это не идеально. Если кто-то сталкивался с этой проблемой в прошлом и нашел решение, я бы очень хотел его услышать.

Обновление:

Я также разместил на Rx форумы, и они будут включать планировщик тестирования в ближайший выпуск. Это, вероятно, будет окончательным решением, когда оно станет доступным.


person Andrew Anderson    schedule 10.06.2010    source источник


Ответы (2)


Проблема вызвана асинхронным характером вызовов, запланированных 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);
   }
}

Затем во время выполнения инициализируйте UISafeScheduler с помощью DispatcherScheduler, а в ваших тестах инициализируйте его с помощью своего поддельного планировщика.

person Samuel Jack    schedule 11.06.2010
comment
Это выглядит лучшим краткосрочным решением - и я особенно ценю элегантность подхода, основанного на методе расширения. - person Andrew Anderson; 11.06.2010

Проблема в том, что MSTest.exe запускает Dispatcher (т.е. Dispatcher.Current! = Null), поэтому ObserveOnDispatcher работает. Однако этот Диспетчер ничего не делает! (т.е. элементы диспетчера в очереди будут проигнорированы) Любой написанный вами код, который явно использует Schedule.Dispatcher, не поддается тестированию

Я решил это перебором в ReactiveUI - вот важные моменты:

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

Мы в основном настраиваем глобальную переменную, которая определяет планировщик по умолчанию, а затем пытаемся определить, когда мы находимся в средстве выполнения тестов.

Затем в наших классах, реализующих IObservable, все принимают параметр IScheduler, значение которого по умолчанию станет глобальным планировщиком по умолчанию. Возможно, я мог бы сделать лучше, но это работает для меня и снова делает код ViewModel пригодным для тестирования.

person Ana Betts    schedule 09.07.2010
comment
Обновление: github.com/reactiveui/ReactiveUI/blob/master/ ReactiveUI / - person superjos; 13.08.2015
comment
Вот рабочая ссылка - person Artemious; 22.10.2020