52

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

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

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

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

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

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

4

16 に答える 16

92

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アプリケーションフレームワーク(WAF)にもあります。

于 2009-10-03T10:12:27.603 に答える
25

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

于 2009-10-14T22:21:18.403 に答える
17

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

ここで知りました。

于 2009-09-17T12:05:57.493 に答える
6

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

次に、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
    }
}
于 2014-04-03T09:59:39.910 に答える
2

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

単体テストの実行中、メイン スレッドがアイドル状態になることはありません。すべてのテストを実行してから終了します。

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


編集(5か月後)DispatcherFrameに気づかずにこの回答を書きました。これについて間違っていたことをとてもうれしく思います - DispatcherFrame は非常に便利であることが判明しました。

于 2009-08-20T03:17:14.603 に答える
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);
}
于 2009-10-14T22:08:06.417 に答える
1

私は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));
    }
于 2014-03-02T12:29:40.427 に答える
1

sにアクセスする際のエラーを回避することが目標である場合はDependencyObject、スレッドを明示的に操作するのではなく、テストが (単一の)スレッドDispatcherで実行されるようにすることをお勧めします。STAThread

これはあなたのニーズに合っているかもしれませんし、合っていないかもしれません.

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

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

DispatcherUtil にもう 1 つのメソッドを追加して DoEventsSync() を呼び出し、BeginInvoke の代わりに Dispatcher を呼び出して 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;
        }
    }
于 2014-06-23T10:53:54.627 に答える
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();
    }
于 2017-10-30T02:09:19.547 に答える
0

これは少し古い投稿です。現在、BeginInvoke は好ましいオプションではありません。私はモックの解決策を探していましたが、InvokeAsync のソリューションが見つかりませんでした:

await App.Current.Dispatcher.InvokeAsync(() => something );

IDispatcher を実装する Dispatcher という新しいクラスを追加し、viewModel コンストラクターに挿入します。

public class Dispatcher : IDispatcher
{
    public async Task DispatchAsync(Action action)
    {
        await App.Current.Dispatcher.InvokeAsync(action);
    }
}
public interface IDispatcher
    {
        Task DispatchAsync(Action action);
    }

次に、テストで、MockDispatcher をコンストラクターの viewModel に挿入しました。

internal class MockDispatcher : IDispatcher
    {
        public async Task DispatchAsync(Action action)
        {
            await Task.Run(action);
        }
    }

ビュー モデルで使用します。

await m_dispatcher.DispatchAsync(() => something);
于 2020-04-09T13:35:37.673 に答える