2 votes

Tester unitaire avec FromAsyncPattern

Les Extensions Réactives ont un petit crochet sexy pour simplifier l'appel des méthodes asynchrones :

var func = Observable.FromAsyncPattern(
    myWcfService.BeginDoStuff,
    myWcfService.EndDoStuff);

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

Je l'utilise dans un projet WPF, et cela fonctionne très bien à l'exécution.

Malheureusement, lors de la tentative de tester unitairement les méthodes utilisant cette technique, je rencontre des échecs aléatoires. Environ 3 tests sur cinq contenant ce code échouent.

Voici un exemple de test (implémenté en utilisant un conteneur de mockage automatique Rhino/Unity) :

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

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

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

   // act
   var target = CreateTestSubject(container);

   target.DoMethodThatInvokesService();

   // Exécuter le dispatcher pour tout à la priorité de fond
   Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new Action(() => { }));

   // assert
   Assert.IsTrue(mon opération s'est bien déroulée);
}

Le problème que je vois est que le code spécifié pour s'exécuter lorsque l'action asynchrone est terminée (dans ce cas, Foo(x)), n'est jamais appelé. Je peux le vérifier en plaçant des points d'arrêt dans Foo et en observant qu'ils ne sont jamais atteints. De plus, je peux forcer un long délai après avoir appelé DoMethodThatInvokesService (qui lance l'appel asynchrone), et le code n'est toujours pas exécuté. Je suis sûr que les lignes de code invoquant le framework Rx ont été appelées.

Autres choses que j'ai essayées :

Cela a amélioré mon taux d'échec à environ 1 sur 5, mais les échecs sont toujours survenus.

  • J'ai réécrit le code Rx pour utiliser le modèle asynchrone classique. Cela fonctionne, cependant mon ego de développeur aimerait vraiment utiliser Rx au lieu de l'ancien modèle begin/end.

En fin de compte, j'ai une solution de contournement (c'est-à-dire ne pas utiliser Rx), cependant je sens que ce n'est pas idéal. Si quelqu'un a rencontré ce problème dans le passé et a trouvé une solution, j'aimerais vraiment l'entendre.

Mise à jour :

J'ai également publié sur les forums Rx, et ils incluront un planificateur de test dans une prochaine version. Ce sera probablement la solution ultime une fois disponible.

6voto

Paul Betts Points 41354

Le problème est que MSTest.exe exécute un Dispatcher (c'est-à-dire Dispatcher.Current != null), donc ObserveOnDispatcher fonctionne. Cependant, ce Dispatcher ne fait rien! (c'est-à-dire que les éléments en file d'attente du Dispatcher seront ignorés) Tout code que vous écrivez qui utilise explicitement Schedule.Dispatcher est inutile

J'ai résolu cela de façon brutale dans ReactiveUI - voici les parties importantes :

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

Nous configurons essentiellement une variable globale qui définit le planificateur par défaut, puis nous essayons de détecter quand nous sommes dans un testeur.

Ensuite, dans nos classes qui implémentent IObservable, nous prenons tous un paramètre IScheduler, dont la valeur par défaut finira par être le planificateur par défaut global. J'aurais sans doute pu faire mieux, mais cela fonctionne pour moi et rend à nouveau le code du ViewModel testable.

3voto

Samuel Jack Points 14556

Le problème est causé par la nature asynchrone des appels planifiés par ObserveOnDispatcher. Vous ne pouvez pas garantir qu'ils sont tous terminés au moment où votre test se termine. Vous devez donc reprendre la planification sous votre contrôle.

Et si vous injectiez le planificateur dans votre classe ?

Ensuite, plutôt que d'appeler ObserveOnDispatcher, vous appelez ObserveOn, en passant l'implémentation de IScheduler qui a été injectée.

À l'exécution, vous injecteriez le DispatcherScheduler, mais dans vos tests, vous injecteriez un faux planificateur qui met en file d'attente toutes les actions qui lui sont données et les exécute à un moment contrôlé par vos tests.

Si vous n'aimez pas l'idée de devoir injecter un planificateur partout où vous utilisez Rx, que diriez-vous de créer votre propre méthode d'extension, quelque chose comme ceci (code non testé) :

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

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

       return source.ObserveOn(UISafeScheduler);
   }
}

Ensuite, à l'exécution, initialisez UISafeScheduler avec DispatcherScheduler, et dans vos tests, initialisez-le avec votre faux planificateur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X