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.

92voto

jbe Points 4629

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

Vous pouvez écrire une simple méthode d'aide "DispatcherUtil.DoEvents()" qui indique au 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 trouverez cette classe également dans le Cadre d'application WPF (WAF) .

4 votes

Je préfère cette réponse à la réponse acceptée, car cette solution peut être exécutée dans un cas de test écrit de manière séquentielle, alors que la réponse acceptée exige que le code de test soit écrit dans une approche orientée callback.

1 votes

Cela ne fonctionne pas pour moi, malheureusement. Cette méthode est également documentée ici pour ceux qui sont intéressés : Exemple de "DoEvents" de DispatcherFrame MSDN

1 votes

Ignorez mes derniers commentaires - cela fonctionne bien et constitue une bonne solution de rechange à ce problème courant lors du test des modèles de vue WPF.

25voto

Orion Edwards Points 54939

Nous avons résolu ce problème en simulant simplement le répartiteur derrière une interface, et en tirant 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 un simulateur que nous fournissons au code pendant les tests unitaires :

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

Nous disposons également d'une variante de la 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.

0 votes

Comment simuler la méthode DispatcherInvoke ?

0 votes

@lukaszk, en fonction de votre cadre de mocking, vous devriez configurer la méthode Invoke sur votre mock pour exécuter réellement le délégué qui lui est passé (si c'est le comportement dont vous avez besoin). Vous n'avez pas nécessairement besoin d'exécuter ce délégué, j'ai quelques tests où je vérifie simplement que le bon délégué a été passé au mock.

0 votes

Pour ceux qui utilisent Moq, voici ce qui a fonctionné pour moi : ` var mockDispatcher = new Mock<IDispatcher>() ; mockDispatcher.Setup(dispatcher => dispatcher.Invoke(It.IsAny<Action>())).Callback<Action>(action => action());`

17voto

StewartArmbrecht Points 179

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

[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.");
}

Je l'ai découvert aquí .

0 votes

Oui, je suis juste revenu pour mettre à jour cette question avec la façon dont je l'ai fait à la fin. J'ai lu le même post, je crois !

6voto

informatorius Points 11

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

Ensuite, toute classe testée qui accède à Application.Current.Dispatcher trouvera un dispatcher.

Parce qu'une seule application est autorisée dans un AppDomain, j'ai utilisé AssemblyInitialize et l'ai mis 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
    }
}

0 votes

J'aime beaucoup ça !

2voto

La création d'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);
}

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