web-dev-qa-db-fra.com

Utilisation du répartiteur WPF dans les tests unitaires

Je ne parviens pas à faire en sorte que Dispatcher dirige un délégué que je lui transmets lors des tests unitaires. Tout fonctionne bien lorsque j'exécute le programme, mais lors du test d'unité, le code suivant ne fonctionnera pas:

this.Dispatcher.BeginInvoke(new ThreadStart(delegate
{
    this.Users.Clear();

    foreach (User user in e.Results)
    {
        this.Users.Add(user);
    }
}), DispatcherPriority.Normal, null);

J'ai ce code dans ma classe de base viewmodel pour obtenir un répartiteur:

if (Application.Current != null)
{
    this.Dispatcher = Application.Current.Dispatcher;
}
else
{
    this.Dispatcher = Dispatcher.CurrentDispatcher;
}

Dois-je faire quelque chose pour initialiser le Dispatcher en vue des tests unitaires? Dispatcher n'exécute jamais le code dans le délégué.

45
Chris Shepherd

En utilisant Visual Studio Unit Test Framework, vous n’avez pas besoin d’initialiser vous-même Dispatcher. Vous avez absolument raison, Dispatcher ne traite pas automatiquement sa file d’attente.

Vous pouvez écrire une méthode d'assistance simple «DispatcherUtil.DoEvents ()» qui indique à Dispatcher de traiter sa file d'attente.

Code C #:

public static class DispatcherUtil
{
    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public static void DoEvents()
    {
        DispatcherFrame frame = new DispatcherFrame();
        Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
            new DispatcherOperationCallback(ExitFrame), frame);
        Dispatcher.PushFrame(frame);
    }

    private static object ExitFrame(object frame)
    {
        ((DispatcherFrame)frame).Continue = false;
        return null;
    }
}

Vous trouvez également cette classe dans le WPF Application Framework (WAF).

83
jbe

Nous avons résolu ce problème en moquant simplement le répartiteur derrière une interface et en extrayant l'interface de notre conteneur IOC. Voici l'interface:

public interface IDispatcher
{
    void Dispatch( Delegate method, params object[] args );
}

Voici l'implémentation concrète enregistrée dans le conteneur IOC pour l'application réelle.

[Export(typeof(IDispatcher))]
public class ApplicationDispatcher : IDispatcher
{
    public void Dispatch( Delegate method, params object[] args )
    { UnderlyingDispatcher.BeginInvoke(method, args); }

    // -----

    Dispatcher UnderlyingDispatcher
    {
        get
        {
            if( App.Current == null )
                throw new InvalidOperationException("You must call this method from within a running WPF application!");

            if( App.Current.Dispatcher == null )
                throw new InvalidOperationException("You must call this method from within a running WPF application with an active dispatcher!");

            return App.Current.Dispatcher;
        }
    }
}

Et voici une maquette que nous fournissons au code lors des tests unitaires:

public class MockDispatcher : IDispatcher
{
    public void Dispatch(Delegate method, params object[] args)
    { method.DynamicInvoke(args); }
}

Nous avons également une variante de la variable MockDispatcher qui exécute les délégués dans un fil d’arrière-plan, mais ce n’est pas nécessaire la plupart du temps.

21
Orion Edwards

Vous pouvez effectuer des tests unitaires à l'aide d'un répartiteur, il vous suffit d'utiliser le DispatcherFrame. Voici un exemple de l'un de mes tests unitaires qui utilise DispatcherFrame pour forcer la file d'attente du répartiteur à s'exécuter.

[TestMethod]
public void DomainCollection_AddDomainObjectFromWorkerThread()
{
 Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
 DispatcherFrame frame = new DispatcherFrame();
 IDomainCollectionMetaData domainCollectionMetaData = this.GenerateIDomainCollectionMetaData();
 IDomainObject parentDomainObject = MockRepository.GenerateMock<IDomainObject>();
 DomainCollection sut = new DomainCollection(dispatcher, domainCollectionMetaData, parentDomainObject);

 IDomainObject domainObject = MockRepository.GenerateMock<IDomainObject>();

 sut.SetAsLoaded();
 bool raisedCollectionChanged = false;
 sut.ObservableCollection.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs e)
 {
  raisedCollectionChanged = true;
  Assert.IsTrue(e.Action == NotifyCollectionChangedAction.Add, "The action was not add.");
  Assert.IsTrue(e.NewStartingIndex == 0, "NewStartingIndex was not 0.");
  Assert.IsTrue(e.NewItems[0] == domainObject, "NewItems not include added domain object.");
  Assert.IsTrue(e.OldItems == null, "OldItems was not null.");
  Assert.IsTrue(e.OldStartingIndex == -1, "OldStartingIndex was not -1.");
  frame.Continue = false;
 };

 WorkerDelegate worker = new WorkerDelegate(delegate(DomainCollection domainCollection)
  {
   domainCollection.Add(domainObject);
  });
 IAsyncResult ar = worker.BeginInvoke(sut, null, null);
 worker.EndInvoke(ar);
 Dispatcher.PushFrame(frame);
 Assert.IsTrue(raisedCollectionChanged, "CollectionChanged event not raised.");
}

J'ai découvert à ce sujet ici .

16
StewartArmbrecht

Créer un DipatcherFrame a bien fonctionné pour moi:

[TestMethod]
public void Search_for_item_returns_one_result()
{
    var searchService = CreateSearchServiceWithExpectedResults("test", 1);
    var eventAggregator = new SimpleEventAggregator();
    var searchViewModel = new SearchViewModel(searchService, 10, eventAggregator) { SearchText = searchText };

    var signal = new AutoResetEvent(false);
    var frame = new DispatcherFrame();

    // set the event to signal the frame
    eventAggregator.Subscribe(new ProgressCompleteEvent(), () =>
       {
           signal.Set();
           frame.Continue = false;
       });

    searchViewModel.Search(); // dispatcher call happening here

    Dispatcher.PushFrame(frame);
    signal.WaitOne();

    Assert.AreEqual(1, searchViewModel.TotalFound);
}
2
Jon Dalberg

Si vous souhaitez appliquer la logique dans la réponse de jbe à tout répartiteur (pas seulement Dispatcher.CurrentDispatcher, vous pouvez utiliser la méthode d'extention suivante.

public static class DispatcherExtentions
{
    public static void PumpUntilDry(this Dispatcher dispatcher)
    {
        DispatcherFrame frame = new DispatcherFrame();
        dispatcher.BeginInvoke(
            new Action(() => frame.Continue = false),
            DispatcherPriority.Background);
        Dispatcher.PushFrame(frame);
    }
}

Usage:

Dispatcher d = getADispatcher();
d.PumpUntilDry();

À utiliser avec le répartiteur actuel:

Dispatcher.CurrentDispatcher.PumpUntilDry();

Je préfère cette variante car elle peut être utilisée dans plus de situations, est implémentée avec moins de code et a une syntaxe plus intuitive.

Pour plus d'informations sur DispatcherFrame, consultez ceci excellent article de blog .

2
Timothy Schoonover

Lorsque vous appelez Dispatcher.BeginInvoke, vous demandez au répartiteur d'exécuter les délégués sur son thread. quand le fil est inactif.

Lors de l'exécution des tests unitaires, le fil principal jamais soyez inactif. Il exécutera tous les tests puis se terminera.

Pour que cette unité d'aspect puisse être testée, vous devrez modifier la conception sous-jacente afin qu'elle n'utilise pas le répartiteur du thread principal. Une autre alternative consiste à utiliser le System.ComponentModel.BackgroundWorker pour modifier les utilisateurs sur un autre thread. (Ceci est juste un exemple, il pourrait être inapproprié selon le contexte).


Éditer (5 mois plus tard) J'ai écrit cette réponse sans connaître le DispatcherFrame. Je suis assez heureux d'avoir eu tort sur ce point. DispatcherFrame s'est révélé extrêmement utile.

2
Andrew Shepherd

J'ai résolu ce problème en créant une nouvelle application dans la configuration de test unitaire.

Ensuite, toute classe sous test dont l'accès à Application.Current.Dispatcher trouvera un répartiteur.

Comme une seule application est autorisée dans un AppDomain, j'ai utilisé AssemblyInitialize et l'ai placé dans sa propre classe ApplicationInitializer.

[TestClass]
public class ApplicationInitializer
{
    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext context)
    {
        var waitForApplicationRun = new TaskCompletionSource<bool>()
        Task.Run(() =>
        {
            var application = new Application();
            application.Startup += (s, e) => { waitForApplicationRun.SetResult(true); };
            application.Run();
        });
        waitForApplicationRun.Task.Wait();        
    }
    [AssemblyCleanup]
    public static void AssemblyCleanup()
    {
        Application.Current.Dispatcher.Invoke(Application.Current.Shutdown);
    }
}
[TestClass]
public class MyTestClass
{
    [TestMethod]
    public void MyTestMethod()
    {
        // implementation can access Application.Current.Dispatcher
    }
}
2
informatorius

Si votre objectif est d'éviter les erreurs lors de l'accès à DependencyObjects, je suggère que, plutôt que de jouer explicitement avec les threads et Dispatcher, vous assurez simplement que vos tests s'exécutent dans un (unique) thread STAThread.

Cela peut correspondre ou non à vos besoins, du moins pour moi cela a toujours été suffisant pour tester quoi que ce soit lié à DependencyObject/WPF.

Si vous souhaitez essayer ceci, je peux vous indiquer plusieurs manières de le faire:

  • Si vous utilisez NUnit> = 2.5.0, un attribut [RequiresSTA] peut cibler des méthodes ou des classes de test. Attention toutefois si vous utilisez un programme d'exécution de test intégré, comme par exemple le programme d'exécution NUnit R # 4.5 semble être basé sur une version plus ancienne de NUnit et ne peut pas utiliser cet attribut.
  • Avec les anciennes versions de NUnit, vous pouvez configurer NUnit pour utiliser un thread [STAThread] avec un fichier de configuration, voir par exemple cet article de blog de Chris Headgate.
  • Enfin, le même article de blog dispose d’une méthode de secours (que j’avais utilisée avec succès par le passé) pour créer votre propre thread [STAThread] sur lequel exécuter votre test.
1
Thomas Dufour

Pourquoi ne pas exécuter le test sur un thread dédié avec le support Dispatcher?

    void RunTestWithDispatcher(Action testAction)
    {
        var thread = new Thread(() =>
        {
            var operation = Dispatcher.CurrentDispatcher.BeginInvoke(testAction);

            operation.Completed += (s, e) =>
            {
                // Dispatcher finishes queued tasks before shuts down at idle priority (important for TransientEventTest)
                Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.ApplicationIdle);
            };

            Dispatcher.Run();
        });

        thread.IsBackground = true;
        thread.TrySetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
    }
0
Esge

J'ai accompli cela en enveloppant Dispatcher dans ma propre interface IDispatcher, puis en utilisant Moq pour vérifier si l'appel avait bien été passé.

Interface IDispatcher:

public interface IDispatcher
{
    void BeginInvoke(Delegate action, params object[] args);
}

Mise en œuvre réelle du dispatcheur:

class RealDispatcher : IDispatcher
{
    private readonly Dispatcher _dispatcher;

    public RealDispatcher(Dispatcher dispatcher)
    {
        _dispatcher = dispatcher;
    }

    public void BeginInvoke(Delegate method, params object[] args)
    {
        _dispatcher.BeginInvoke(method, args);
    }
}

Initialisation du répartiteur dans votre classe sous test:

public ClassUnderTest(IDispatcher dispatcher = null)
{
    _dispatcher = dispatcher ?? new UiDispatcher(Application.Current?.Dispatcher);
}

Se moquer du répartiteur à l'intérieur des tests unitaires (dans ce cas, mon gestionnaire d'événements est OnMyEventHandler et accepte un seul paramètre bool appelé myBoolParameter)

[Test]
public void When_DoSomething_Then_InvokeMyEventHandler()
{
    var dispatcher = new Mock<IDispatcher>();

    ClassUnderTest classUnderTest = new ClassUnderTest(dispatcher.Object);

    Action<bool> OnMyEventHanlder = delegate (bool myBoolParameter) { };
    classUnderTest.OnMyEvent += OnMyEventHanlder;

    classUnderTest.DoSomething();

    //verify that OnMyEventHandler is invoked with 'false' argument passed in
    dispatcher.Verify(p => p.BeginInvoke(OnMyEventHanlder, false), Times.Once);
}
0
Eternal21

Je suggère d'ajouter une méthode supplémentaire à DispatcherUtil, appelez-la DoEventsSync () et appelez simplement Dispatcher à appeler au lieu de BeginInvoke. Cela est nécessaire si vous devez vraiment attendre que Dispatcher ait traité toutes les images. Je poste ceci comme une autre réponse, pas seulement un commentaire, puisque toute la classe est trop longue:

    public static class DispatcherUtil
    {
        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void DoEvents()
        {
            var frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrame), frame);
            Dispatcher.PushFrame(frame);
        }

        public static void DoEventsSync()
        {
            var frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrame), frame);
            Dispatcher.PushFrame(frame);
        }

        private static object ExitFrame(object frame)
        {
            ((DispatcherFrame)frame).Continue = false;
            return null;
        }
    }
0
thewhiteambit

Je suis en retard mais voici comment je le fais:

public static void RunMessageLoop(Func<Task> action)
{
  var originalContext = SynchronizationContext.Current;
  Exception exception = null;
  try
  {
    SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());

    action.Invoke().ContinueWith(t =>
    {
      exception = t.Exception;
    }, TaskContinuationOptions.OnlyOnFaulted).ContinueWith(t => Dispatcher.ExitAllFrames(),
      TaskScheduler.FromCurrentSynchronizationContext());

    Dispatcher.Run();
  }
  finally
  {
    SynchronizationContext.SetSynchronizationContext(originalContext);
  }
  if (exception != null) throw exception;
}
0
Andreas Zita

J'utilise les technologies MSTest et Windows Forms avec le paradigme MVVM . Après avoir essayé de nombreuses solutions, enfin, ce (trouvé sur le blog de Vincent Grondin) fonctionne pour moi:

    internal Thread CreateDispatcher()
    {
        var dispatcherReadyEvent = new ManualResetEvent(false);

        var dispatcherThread = new Thread(() =>
        {
            // This is here just to force the dispatcher 
            // infrastructure to be setup on this thread
            Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => { }));

            // Run the dispatcher so it starts processing the message 
            // loop dispatcher
            dispatcherReadyEvent.Set();
            Dispatcher.Run();
        });

        dispatcherThread.SetApartmentState(ApartmentState.STA);
        dispatcherThread.IsBackground = true;
        dispatcherThread.Start();

        dispatcherReadyEvent.WaitOne();
        SynchronizationContext
           .SetSynchronizationContext(new DispatcherSynchronizationContext());
        return dispatcherThread;
    }

Et utilisez-le comme:

    [TestMethod]
    public void Foo()
    {
        Dispatcher
           .FromThread(CreateDispatcher())
                   .Invoke(DispatcherPriority.Background, new DispatcherDelegate(() =>
        {
            _barViewModel.Command.Executed += (sender, args) => _done.Set();
            _barViewModel.Command.DoExecute();
        }));

        Assert.IsTrue(_done.WaitOne(WAIT_TIME));
    }
0
Tomasito