Pytanie Używanie kontrolera WPF w testach jednostkowych


Mam problem z wysłaniem do dyspozytora delegata, który przechodzę do niego, gdy testuję urządzenie. Wszystko działa poprawnie, gdy uruchomię program, ale podczas testu jednostki następujący kod nie zostanie uruchomiony:

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

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

Mam ten kod w mojej klasie podstawowej viewmodel, aby uzyskać Dispatchera:

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

Czy jest coś, co muszę zrobić, aby zainicjować Dispatchera w celu przeprowadzenia testów jednostkowych? Dyspozytor nigdy nie uruchamia kodu w delegacie.


45
2017-07-09 23:01


pochodzenie


Jaki błąd otrzymujesz? - Reed Copsey
Nie otrzymuję błędu. To, co jest przekazywane do BeginInvoke na Dispatcherze, nigdy nie działa. - Chris Shepherd
Będę szczery i powiem, że nie musiałem testować modelu widoku, który jeszcze używa dispatchera. Czy to możliwe, że dyspozytor nie działa. Czy wywołanie funkcji Dispatcher.CurrentDispatcher.Run () w twojej pomocy testu? Jestem ciekawy, więc postuj wyniki, jeśli je otrzymasz. - Anderson Imes


Odpowiedzi:


Korzystając z Visual Studio Unit Test Framework nie musisz sam inicjować Dispatchera. Masz całkowitą rację, że Dyspozytor nie przetwarza automatycznie swojej kolejki.

Możesz napisać prostą metodę pomocniczą "DispatcherUtil.DoEvents ()", która nakazuje Dispatcherowi przetworzyć jej kolejkę.

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

Ta klasa też znajduje się w WPF Application Framework (WAF).


82
2017-10-03 10:12



Wolę tę odpowiedź od zaakceptowanej odpowiedzi, ponieważ to rozwiązanie może być uruchamiane w sekwencyjnie utworzonym teście testowym, podczas gdy zaakceptowana odpowiedź wymaga, aby kod testowy był napisany w sposób zorientowany na wywołanie zwrotne. - Patrick Linskey
Znakomity. Dziękuję za udostępnienie. - ozczecho
To działało znakomicie dla mnie. To jest moja akceptowana odpowiedź - Edward Wilde
To nie działa dla mnie niestety. Ta metoda jest również udokumentowana tutaj dla zainteresowanych: MSDN DispatcherFrame Przykład "DoEvents" - dodgy_coder
Twoje DoEvents będą ryzykować wykonywanie wywołań zaplanowanych przez inne testy jednostkowe, co powoduje awarie, które są bardzo trudne do debugowania :-( Znalazłem ten post, ponieważ ktoś dodał dosłownie kopię przykładowego kodu DispatcherUtil do naszych testów jednostkowych i spowodował ten problem.Myślę, że ukrywanie dyspozytora za interfejsem sugerowanym przez @OrionEdwards to lepsze podejście, chociaż korzystałbym z implementacji z rzeczywistą kolejką i metodą do wyraźnego odciągania w testach jednostkowych. Jeśli przejdę do implementacji, dodam lub edytuję odpowiedź tutaj. - Wim Coenen


Rozwiązaliśmy ten problem, po prostu wyśmiewając dyspozytora za interfejs i wciągając interfejs z naszego kontenera IOC. Oto interfejs:

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

Oto konkretna implementacja zarejestrowana w kontenerze IOC dla prawdziwej aplikacji

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

A oto fałszywy, który dostarczamy do kodu podczas testów jednostkowych:

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

Mamy również wariant MockDispatcher który wykonuje delegatów w wątku tła, ale nie jest to konieczne przez większość czasu


20
2017-10-14 22:21



Jak kpić z metody DispatcherInvoke? - lukaszk
@lukaszk, w zależności od twojego szyderczego środowiska, skonfigurowałbyś metodę Invoke na swoim symulatorze, aby faktycznie uruchomić delegata przekazanego do niego (jeśli takie zachowanie było wymagane). Nie musisz koniecznie uruchamiać tego delegata, mam kilka testów, w których po prostu sprawdzam, czy właściwy delegat został przekazany do fałszywek. - Doctor Jones


Możesz testować jednostki za pomocą dispatchera, wystarczy użyć DispatcherFrame. Oto przykład jednego z moich testów jednostkowych, który wykorzystuje DispatcherFrame, aby wymusić wykonanie kolejki dyspozytora.

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

Dowiedziałem się o tym tutaj.


15
2017-09-17 12:05



Tak, właśnie wróciłem, aby zaktualizować to pytanie, w jaki sposób zrobiłem to w końcu. Czytam ten sam post, jak myślę! - Chris Shepherd


Kiedy wywołujesz Dispatcher.BeginInvoke, instruujesz dyspozytora, aby uruchamiał delegatów w swoim wątku gdy wątek jest bezczynny.

Podczas wykonywania testów jednostkowych główny wątek będzie nigdy bądź bezczynny. Spowoduje to zakończenie wszystkich testów.

Aby ta jednostka aspektu mogła być przetestowana, będziesz musiał zmienić projekt bazowy, aby nie korzystał z dyspozytora głównego wątku. Inną alternatywą jest wykorzystanie System.ComponentModel.BackgroundWorker aby zmodyfikować użytkowników w innym wątku. (To tylko przykład, może być nieodpowiedni w zależności od kontekstu).


Edytować (5 miesięcy później) Napisałem tę odpowiedź, nie zdając sobie sprawy z DispatcherFrame. Cieszę się, że popełniłem błąd na tym - DispatcherFrame okazał się niezwykle przydatny.


2
2017-08-20 03:17





Utworzenie DipatcherFrame działało dobrze dla mnie:

[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
2017-10-14 22:08





Jeśli chcesz zastosować logikę w Odpowiedź Jbe do każdy dyspozytor (nie tylko Dispatcher.CurrentDispatcher, możesz użyć następującej metody rozszerzenia.

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

Stosowanie:

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

Aby użyć z bieżącym dyspozytorem:

Dispatcher.CurrentDispatcher.PumpUntilDry();

Wolę tę odmianę, ponieważ może być używana w większej liczbie sytuacji, jest zaimplementowana przy użyciu mniejszej ilości kodu i ma bardziej intuicyjną składnię.

Aby uzyskać dodatkowe tło na DispatcherFrame, Sprawdź to doskonałe pisanie bloga.


2
2018-04-15 19:20



to dziwna nazwa metody ... - d.moncada
Dispatcher.PushFrame(frame); używa Dispatcher.CurrentDispatcher wewnętrznie ... Więc to by nie zadziałało. - ManIkWeet


Rozwiązałem ten problem, tworząc nową aplikację w konfiguracji testu urządzenia.

Następnie każda badana klasa, która uzyska dostęp do aplikacji Application.Current.Dispatcher.

Ponieważ w aplikacji AppDomain dozwolona jest tylko jedna aplikacja, skorzystałem z AssemblyInitialize i umieściłem ją w swojej klasie 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
2018-04-03 09:59





Jeśli Twoim celem jest uniknięcie błędów podczas uzyskiwania dostępu DependencyObjects, sugeruję, że zamiast grać z wątkami i Dispatcher wyraźnie, po prostu upewnij się, że twoje testy działają w (pojedynczej) STAThread wątek.

To może, ale nie musi, odpowiadać twoim potrzebom, dla mnie przynajmniej zawsze wystarczało do testowania czegokolwiek DependencyObject / związanego z WPF.

Jeśli chcesz tego spróbować, mogę wskazać ci kilka sposobów, aby to zrobić:

  • Jeśli używasz NUnit> = 2.5.0, istnieje [RequiresSTA] atrybut, który może kierować na metody testowe lub klasy. Uważaj jednak, jeśli używasz zintegrowanego testera, tak jak na przykład runner NUnit R # 4.5 wydaje się być oparty na starszej wersji NUnit i nie może używać tego atrybutu.
  • W starszych wersjach NUnit możesz ustawić NUnit na użycie a [STAThread] wątek z plikiem konfiguracyjnym, patrz na przykład ten wpis na blogu autor: Chris Headgate.
  • Wreszcie, ten sam wpis na blogu ma metodę awaryjną (którą z powodzeniem stosowałem w przeszłości) do tworzenia własnych [STAThread] wątek, aby uruchomić test.

0
2017-08-20 09:31





używam MSTest i Windows Forms technologia z paradygmatem MVVM. Po wypróbowaniu wielu rozwiązań w końcu to (znaleziono na blogu Vincenta Grondina) pracuje dla mnie:

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

I użyj go jak:

    [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
2018-03-02 12:29





Sugeruję dodanie jeszcze jednej metody do DispatcherUtil, wywołując ją DoEventsSync () i po prostu wywołaj Dispatchera w celu Invoke zamiast BeginInvoke. Jest to konieczne, jeśli naprawdę musisz poczekać, aż Dyspozytor przetworzy wszystkie ramki. Zamieszczam to jako kolejną odpowiedź nie tylko komentarz, ponieważ cała klasa ma długo:

    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
2018-06-23 10:53





Osiągnąłem to, owijając Dispatchera we własny interfejs IDispatchera, a następnie używając Moq do sprawdzenia, czy zostało wykonane wywołanie.

Interfejs IDispatcher:

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

Prawdziwa implementacja dyspozytora:

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

Inicjowanie dyspozytora w testowanej klasie:

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

Prześmiewanie dyspozytora w testach jednostkowych (w tym przypadku moja procedura obsługi zdarzeń to OnMyEventHandler i akceptuje pojedynczy parametr bool o nazwie 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
2017-08-23 12:26