Модульное тестирование с FromAsyncPattern - PullRequest
2 голосов
/ 10 июня 2010

У 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, чтобы использовать шаблон Async с простым jane. Это работает, однако мое эго разработчика действительно хотело бы использовать Rx вместо скучного старого начала / конца.

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

Обновление

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

Ответы [ 2 ]

6 голосов
/ 09 июля 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 снова тестируемым.

3 голосов
/ 11 июня 2010

Проблема вызвана асинхронным характером вызовов, запланированных на 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, а в своих тестах инициализируйте его с помощью своего поддельного планировщика.

...