2009-07-09 82 views
45

我很難讓調度程序運行我在單元測試時傳遞給它的委託。一切正常,當我運行程序很好,但是,一個單元測試期間,下面的代碼將不會運行:在單元測試中使用WPF調度程序

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

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

我在我的ViewModel基類的代碼獲得接線員:

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

有什麼我需要做的初始化單元測試Dispatcher? Dispatcher從不在委託中運行代碼。

+0

您收到什麼錯誤? – 2009-07-09 23:19:50

+0

我沒有得到任何錯誤。正是在Dispatcher上傳遞給BeginInvoke的內容永遠不會運行。 – 2009-07-10 20:27:01

+1

我會誠實地說,我沒有必須單元測試一個利用調度器的視圖模型。是否有可能調度員沒有運行。在你的測試中調用Dispatcher.CurrentDispatcher.Run()會有幫助嗎?我很好奇,所以發佈結果,如果你得到它們。 – 2009-07-30 03:32:32

回答

82

通過使用Visual Studio單元測試框架,您不需要自己初始化Dispatcher。你是對的,分派器不會自動處理隊列。

您可以編寫一個簡單的輔助方法「DispatcherUtil.DoEvents()」,它告訴分派器處理其隊列。

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

您在WPF Application Framework (WAF)找到這個課呢。

+4

我更喜歡接受答案的答案,因爲此解決方案可以按順序編寫的測試用例運行,而接受的答案要求測試代碼用面向回調的方法編寫。 – 2010-09-23 20:54:02

2

當您調用Dispatcher.BeginInvoke時,當線程空閒時,指示調度程序在其線程上運行代理程序

當運行單元測試時,主線程將永不空閒。它將運行所有的測試,然後終止。

要使此方面的單元可測試,您必須更改底層設計,以便它不使用主線程的調度程序。另一種方法是利用System.ComponentModel.BackgroundWorker來修改不同線程上的用戶。 (這只是一個例子,根據具體情況它可能不適用)。


編輯(5個月後) 我寫這個答案雖然不知道DispatcherFrame的。我很高興在這個問題上出錯了--DispatcherFrame變得非常有用。

0

如果你的目標是訪問DependencyObject■當以避免錯誤,我建議,而不是線程和Dispatcher打明確,你只需確保在(單)STAThread線程運行測試。

這可能會也可能不適合您的需求,對我來說,至少對於測試任何與DependencyObject/WPF相關的東西來說,這已經足夠了。

如果你想試試這個,我可以指出你幾種方法可以做到這一點:如果你使用NUnit> = 2.5.0,有一個[RequiresSTA]屬性,可以針對測試方法或類

  • 。請注意,如果您使用集成的測試運行器,例如R#4.5 NUnit runner似乎基於舊版本的NUnit,並且無法使用此屬性。
  • 對於較舊的NUnit版本,可以將NUnit設置爲使用配置文件中的[STAThread]線程,請參閱Chris Headgate的示例this blog post
  • 最後,the same blog post有一個回退方法(我已經成功用於過去)創建自己的[STAThread]線程來運行測試。
15

您可以使用調度程序進行單元測試,您只需使用DispatcherFrame即可。以下是我的一個單元測試示例,它使用DispatcherFrame強制執行調度程序隊列。

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

我知道了吧here

+0

是的,剛回來更新這個問題,最後我是怎麼做到的。我看過同樣的帖子,我想! – 2009-09-28 18:49:17

2

創建DipatcherFrame對我來說真是棒極了:

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

我們通過簡單地嘲諷了接口背後的調度,並從我們的IOC容器接口拉動解決了這個問題。這裏的接口:

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

下面是IOC容器註冊了真正的應用程序

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

的具體實施而這裏的一個模擬的一個,我們在單元測試提供給代碼:

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

我們也有一個MockDispatcher的變體,它在後臺線程中執行委託,但大部分時間不是必需的

2

如果要將jbe's answer中的邏輯應用於任何調度程序(不僅僅是Dispatcher.CurrentDispatcher,您可以使用以下擴展方法。

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

用法:

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

要與當前的調度使用:

Dispatcher.CurrentDispatcher.PumpUntilDry(); 

我喜歡這種變化,因爲它可以在更多的情況下使用,使用更少的代碼實現,有一個更直觀的語法。

有關DispatcherFrame的更多背景,請查看excellent blog writeup

0

我使用的是帶有MVVM範例的MSTestWindows Forms技術。

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

而且使用它像: 嘗試了許多解決方案本終於作品(found on Vincent Grondin blog)我後

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

我創造我的單元測試設置一個新的應用程序解決了這個問題。

然後,任何訪問Application.Current.Dispatcher的待測試類將找到一個調度程序。

因爲在AppDomain中只允許有一個應用程序,所以我使用了AssemblyInitialize並將它放入它自己的類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

我建議在DispatcherUtil中調用它的DoEventsSync()方法,並調用Dispatcher來調用而不是BeginInvoke。如果您真的必須等到分派器處理完所有幀,才需要這樣做。我張貼這是另一種答案不只是一個評論,因爲整個類是長:

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

我在我自己的IDispatcher界面包調度程序,然後使用起訂量來驗證調用它完成了這個製作。

IDispatcher接口:

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

真實調度實現:

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

在類初始化調度下測試:

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

嘲笑裏面的單元測試調度員(在這種情況下,我的事件處理程序是OnMyEventHandler並接受一個單一的布爾參數ca lled 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

如何在具有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(); 
    }