53 votes

Utilisation du dispatcher WPF dans les tests unitaires

J'ai du mal à faire en sorte que le répartiteur exécute un délégué que je lui passe lors des tests unitaires. Tout fonctionne bien lorsque j'exécute le programme, mais, lors d'un test unitaire, le code suivant ne s'exécute 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 Dispatcher :

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

Y a-t-il quelque chose que je doive faire pour initialiser le Dispatcher pour les tests unitaires ? Le Dispatcher n'exécute jamais le code dans le délégué.

0 votes

Je n'obtiens aucune erreur. Ce qui est passé à BeginInvoke sur le Dispatcher ne s'exécute jamais.

1 votes

Je vais être honnête et dire que je n'ai pas encore eu à tester unitairement un modèle de vue qui utilise un répartiteur. Est-il possible que le répartiteur ne fonctionne pas ? L'appel à Dispatcher.CurrentDispatcher.Run() dans votre test serait-il utile ? Je suis curieux, alors postez les résultats si vous les obtenez.

2voto

odysseus.section9 Points 693

Si vous voulez appliquer la logique dans réponse de jbe à tout répartiteur (pas seulement Dispatcher.CurrentDispatcher vous pouvez utiliser la méthode d'extension 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);
    }
}

Utilisation :

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 un plus grand nombre de situations, est mise en œuvre en utilisant moins de code et possède une syntaxe plus intuitive.

Pour de plus amples informations sur DispatcherFrame regardez ça excellent blog writeup .

0 votes

Dispatcher.PushFrame(frame); utilise Dispatcher.CurrentDispatcher en interne... Donc ça ne marcherait pas.

2voto

Andrew Shepherd Points 16670

Lorsque vous appelez Dispatcher.BeginInvoke, vous demandez au distributeur d'exécuter les délégués sur son fil d'exécution. lorsque le fil est inactif .

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

Pour rendre cet aspect testable à l'unité, vous devrez modifier la conception sous-jacente afin de ne pas utiliser le distributeur du thread principal. Une autre alternative est d'utiliser la fonction Système.ComponentModel.BackgroundWorker pour modifier les utilisateurs sur un autre fil. (Ceci n'est qu'un exemple, il pourrait être inapproprié selon le contexte).


Modifier (5 mois plus tard) J'ai écrit cette réponse alors que je ne connaissais pas le DispatcherFrame. Je suis très heureux d'avoir eu tort sur ce point - DispatcherFrame s'est avéré extrêmement utile.

1voto

Thomas Dufour Points 739

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

Cela peut ou non répondre à vos besoins, pour moi en tout cas, cela a toujours été suffisant pour tester tout ce qui concerne DependencyObject/WPF.

Si vous souhaitez essayer, je peux vous indiquer plusieurs façons de le faire :

  • Si vous utilisez NUnit >= 2.5.0, il y a une [RequiresSTA] qui peut cibler des méthodes ou des classes de test. Attention cependant si vous utilisez un exécuteur de test intégré, comme par exemple l'exécuteur NUnit R#4.5 qui semble être basé sur une ancienne version de NUnit et ne peut pas utiliser cet attribut.
  • Avec les anciennes versions de NUnit, vous pouvez configurer NUnit pour qu'il utilise un fichier de type [STAThread] avec un fichier de configuration, voir par exemple cet article de blog par Chris Headgate.
  • Enfin, le même article de blog a une méthode de secours (que j'ai utilisée avec succès dans le passé) pour créer votre propre [STAThread] pour exécuter votre test.

0 votes

Pour les tests unitaires de très bas niveau, cela fonctionne, mais parfois vous devez tester vos fonctions avec des threads en arrière-plan.

1voto

Tomasito Points 416

J'utilise MSTest y Windows Forms avec le paradigme MVVM. Après avoir essayé de nombreuses solutions, finalement cette (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 utilise-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));
    }

1voto

Eternal21 Points 430

J'ai réalisé cela en enveloppant Dispatcher dans ma propre interface IDispatcher, puis en utilisant Moq pour vérifier que l'appel à cette interface a bien été effectué.

Interface IDispatcher :

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

Mise en œuvre réelle du répartiteur :

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 en cours de test :

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

Mocking du distributeur dans les tests unitaires (dans ce cas, mon gestionnaire d'événement 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);
}

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