web-dev-qa-db-ja.com

単体テストでのWPFディスパッチャーの使用

ユニットテスト時にDispatcherに渡すデリゲートを実行するのに問題があります。プログラムを実行しているときはすべて正常に動作しますが、単体テスト中は次のコードは実行されません。

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

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

Dispatcherを取得するために、viewmodel基本クラスに次のコードがあります。

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

ユニットテスト用にDispatcherを初期化するために必要なことはありますか? Dispatcherがデリゲートでコードを実行することはありません。

47
Chris Shepherd

Visual Studioユニットテストフレームワークを使用することで、ディスパッチャーを自分で初期化する必要はありません。 Dispatcherがキューを自動的に処理しないというのは、あなたの言うとおりです。

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)にあります。

86
jbe

この問題は、ディスパッチャをインターフェイスの背後にモックアウトし、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のバリアントもありますが、ほとんどの場合は必要ありません。

21
Orion Edwards

ディスパッチャーを使用して単体テストを実行できます。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.");
}

私はそれについて知りました ここ

17

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);
}
2
Jon Dalberg

jbe's answer のロジックをanyディスパッチャーに適用したい場合(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の背景については、こちら 優れたブログ記事 をご覧ください。

2

Dispatcher.BeginInvokeを呼び出すと、そのスレッドでデリゲートを実行するようにディスパッチャーに指示します スレッドがアイドル状態のとき

ユニットテストを実行すると、メインスレッドは 決して アイドルになります。すべてのテストを実行して終了します。

このアスペクトユニットをテスト可能にするには、メインスレッドのディスパッチャーを使用しないように、基本となる設計を変更する必要があります。別の選択肢は、 System.ComponentModel.BackgroundWorker 別のスレッドでユーザーを変更します。 (これは単なる例であり、状況によっては不適切な場合があります)。


編集(5か月後)DispatcherFrameを知らないうちにこの回答を書きました。私はこれについて間違っていたことをとても嬉しく思います-DispatcherFrameは非常に便利であることがわかりました。

2
Andrew Shepherd

ユニットテストのセットアップで新しいアプリケーションを作成することにより、この問題を解決しました。

次に、Application.Current.Dispatcherにアクセスするテスト中のクラスがディスパッチャーを見つけます。

AppDomainでは1つのアプリケーションしか許可されていないため、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
    }
}
2
informatorius

DependencyObjectsへのアクセス時のエラーを回避することが目的の場合は、スレッドとDispatcherを明示的に操作するのではなく、テストを(単一の)STAThreadスレッド。

これはあなたのニーズに合うかもしれませんし、そうでないかもしれません。少なくとも私にとっては、DependencyObject/WPF関連のテストには常に十分でした。

これを試してみたい場合は、いくつかの方法を紹介します。

  • NUnit> = 2.5.0を使用する場合、テストメソッドまたはクラスを対象とする[RequiresSTA]属性があります。ただし、統合テストランナーを使用する場合は注意してください。たとえば、R#4.5 NUnitランナーは古いバージョンのNUnitに基づいているようで、この属性を使用できません。
  • 古いNUnitバージョンでは、設定ファイルで[STAThread]スレッドを使用するようにNUnitを設定できます。たとえば、Chris Headgateによる このブログ投稿 を参照してください。
  • 最後に、 同じブログ投稿 には、テストを実行するための独自の[STAThread]スレッドを作成するためのフォールバックメソッド(私は過去に正常に使用しました)があります。
1
Thomas Dufour

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();
    }
0
Esge

Dispatcherを自分のIDispatcherインターフェイスでラップし、Moqを使用して呼び出しが行われたことを確認することで、これを実現しました。

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であり、myBoolParameterと呼ばれる単一のboolパラメーターを受け入れます)

[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

DispatcherUtilにメソッドを1つ追加して、それをDoEventsSync()と呼び、DispatcherをBeginInvokeではなくInvokeに呼び出すことをお勧めします。これは、Dispatcherがすべてのフレームを処理するまで本当に待機する必要がある場合に必要です。クラス全体が長くなるので、私はこれをコメントではなく別の回答として投稿しています:

    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

私は遅れていますが、これは私がそれを行う方法です:

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

私が見つけた最も簡単な方法は、このようなプロパティをDispatcherを使用する必要があるすべてのViewModelに追加することです。

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

これにより、アプリケーションと単体テストの実行時の両方で機能します。

アプリケーション全体の数か所で使用するだけでよいので、少し繰り返してもかまいません。

0
Shahin Dohan

MVVMパラダイムでMSTestおよびWindows Formsテクノロジーを使用しています。最後に多くの解決策を試した後、これは (Vincent Grondinブログで見つかります) は私にとってはうまくいきます:

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

そしてそれを次のように使用します:

    [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