it-swarm.com.de

Verwendung des WPF-Dispatchers in Komponententests

Ich habe Probleme, den Dispatcher dazu zu bringen, einen Delegierten zu leiten. Alles funktioniert gut, wenn ich das Programm ausführe, aber während eines Komponententests wird der folgende Code nicht ausgeführt:

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

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

Ich habe diesen Code in meiner Viewmodel-Basisklasse, um einen Dispatcher zu erhalten:

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

Muss ich etwas tun, um den Dispatcher für Komponententests zu initialisieren? Der Dispatcher führt den Code niemals im Delegaten aus.

45
Chris Shepherd

Wenn Sie das Visual Studio Unit Test Framework verwenden, müssen Sie den Dispatcher nicht selbst initialisieren. Sie haben absolut Recht, dass der Dispatcher seine Warteschlange nicht automatisch verarbeitet.

Sie können eine einfache Hilfsmethode "DispatcherUtil.DoEvents ()" schreiben, die den Dispatcher anweist, seine Warteschlange zu verarbeiten.

C # -Code:

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;
    }
}

Sie finden diese Klasse auch im WPF Application Framework (WAF).

83
jbe

Wir haben dieses Problem gelöst, indem Sie einfach den Dispatcher hinter einer Schnittstelle verspottet haben und die Schnittstelle aus unserem Container IOC ziehen. Hier ist die Schnittstelle:

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

Hier ist die konkrete Implementierung, die im Container IOC für die echte App registriert ist

[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;
        }
    }
}

Und hier ist ein Schein, den wir dem Code während Komponententests zur Verfügung stellen:

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

Wir haben auch eine Variante der MockDispatcher, die Delegierte in einem Hintergrundthread ausführt, die jedoch meistens nicht erforderlich ist

21
Orion Edwards

Sie können einen Unit-Test mit einem Dispatcher durchführen. Sie müssen lediglich den DispatcherFrame verwenden. Hier ist ein Beispiel eines meiner Unit-Tests, bei dem DispatcherFrame verwendet wird, um die Ausführung der Dispatcher-Warteschlange zu erzwingen.

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

Ich habe davon erfahren hier .

16

Das Erstellen eines DipatcherFrames hat für mich sehr gut funktioniert:

[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

Wenn Sie die Logik in jbes Antwort auf any - Dispatcher anwenden möchten (nicht nur Dispatcher.CurrentDispatcher), können Sie die folgende Erweiterungsmethode verwenden.

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);
    }
}

Verwendungszweck:

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

Verwendung mit dem aktuellen Dispatcher:

Dispatcher.CurrentDispatcher.PumpUntilDry();

Ich bevorzuge diese Variante, weil sie in mehr Situationen verwendet werden kann, mit weniger Code implementiert wird und eine intuitivere Syntax aufweist.

Weitere Hintergrundinformationen zu DispatcherFrame finden Sie unter Hervorragender Blogbeitrag .

2

Wenn Sie Dispatcher.BeginInvoke aufrufen, weisen Sie den Dispatcher an, die Delegaten in seinem Thread auszuführen wenn der Thread im Leerlauf ist.

Beim Ausführen von Komponententests wird der Haupt-Thread ausgeführt noch nie untätig sein Es werden alle Tests ausgeführt und dann abgebrochen.

Um diese Aspekteinheit testbar zu machen, müssen Sie das zugrunde liegende Design so ändern, dass es nicht den Dispatcher des Haupt-Threads verwendet. Eine andere Alternative ist die Verwendung der System.ComponentModel.BackgroundWorker um die Benutzer in einem anderen Thread zu ändern. (Dies ist nur ein Beispiel, es kann je nach Kontext unangemessen sein).


Edit (5 Monate später) Ich habe diese Antwort geschrieben, ohne den DispatcherFrame zu kennen. Ich bin ziemlich glücklich, dass ich mich in diesem Punkt geirrt habe - DispatcherFrame hat sich als äußerst nützlich erwiesen.

2
Andrew Shepherd

Ich habe dieses Problem gelöst, indem ich in meinem Gerätetest eine neue Anwendung erstellt habe.

Dann wird jede Klasse, die getestet wird, für den Zugriff auf Application.Current.Dispatcher ein Dispatcher gefunden.

Da in einer AppDomain nur eine Anwendung zulässig ist, habe ich AssemblyInitialize verwendet und in ihre eigene Klasse ApplicationInitializer aufgenommen.

[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

Wenn Sie Fehler beim Zugriff auf DependencyObjects vermeiden möchten, schlage ich vor, dass Sie, anstatt explizit mit Threads und Dispatcher zu spielen, sicherstellen, dass Ihre Tests in einem (einzigen) STAThread-Thread ausgeführt werden.

Dies mag vielleicht nicht Ihren Bedürfnissen entsprechen, zumindest war es für mich immer ausreichend, alles zu testen, was mit DependencyObject/WPF zusammenhängt.

Wenn Sie dies versuchen möchten, kann ich Ihnen verschiedene Wege aufzeigen:

  • Wenn Sie NUnit> = 2.5.0 verwenden, gibt es ein [RequiresSTA]-Attribut, das auf Testmethoden oder -klassen abzielen kann. Seien Sie jedoch vorsichtig, wenn Sie einen integrierten Testläufer verwenden, da der R # 4.5-NUnit-Läufer auf einer älteren Version von NUnit zu basieren scheint und dieses Attribut nicht verwenden kann.
  • Bei älteren NUnit-Versionen können Sie NUnit so einstellen, dass ein [STAThread]-Thread mit einer Konfigurationsdatei verwendet wird. Siehe beispielsweise diesen Blogbeitrag von Chris Headgate.
  • Schließlich hat derselbe Blogbeitrag eine Fallback-Methode (die ich in der Vergangenheit bereits erfolgreich eingesetzt habe) zum Erstellen eines eigenen [STAThread]-Threads, auf dem der Test ausgeführt werden kann.
1
Thomas Dufour

Wie wäre es, wenn Sie den Test in einem dedizierten Thread mit Dispatcher-Unterstützung ausführen?

    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

Ich habe dies erreicht, indem ich Dispatcher in meine eigene IDispatcher-Benutzeroberfläche packte und dann mit Moq überprüfte, ob der Aufruf dazu gemacht wurde.

IDispatcher-Schnittstelle:

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

Echte Dispatcher-Implementierung:

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);
    }
}

Dispatcher in der zu testenden Klasse initialisieren:

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

Verspottung des Dispatchers innerhalb von Unit-Tests (in diesem Fall ist mein Event-Handler OnMyEventHandler und akzeptiert einen einzelnen Bool-Parameter namens 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

Ich empfehle, dem Dispatcher eine weitere Methode hinzuzufügen, nennen Sie es DoEventsSync () und rufen Sie einfach den Dispatcher zum Aufruf statt BeginInvoke auf. Dies ist erforderlich, wenn Sie wirklich warten müssen, bis der Dispatcher alle Frames verarbeitet hat. Ich poste dies als weitere Antwort nicht nur als Kommentar, da die ganze Klasse zu lang ist:

    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

Ich bin spät dran, aber so mache ich es:

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

Der einfachste Weg, den ich gefunden habe, besteht darin, jedem ViewModel, das den Dispatcher verwenden muss, eine Eigenschaft wie diese hinzuzufügen:

public static Dispatcher Dispatcher => Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;

Auf diese Weise funktioniert es sowohl in der Anwendung als auch beim Ausführen von Komponententests.

Ich musste es in meiner gesamten Anwendung nur an wenigen Stellen verwenden, sodass es mir nichts ausmachte, mich ein wenig zu wiederholen.

0
Shahin Dohan

Ich verwende die MSTest- und Windows Forms-Technologie mit dem MVVM-Paradigma . Nachdem ich viele Lösungen ausprobiert hatte, funktioniert diese (im Vincent Grondin Blog) für mich:

    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;
    }

Und benutze es wie:

    [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