ユニットテスト時に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がデリゲートでコードを実行することはありません。
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)にあります。
この問題は、ディスパッチャをインターフェイスの背後にモックアウトし、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
のバリアントもありますが、ほとんどの場合は必要ありません。
ディスパッチャーを使用して単体テストを実行できます。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.");
}
私はそれについて知りました ここ 。
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);
}
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
の背景については、こちら 優れたブログ記事 をご覧ください。
Dispatcher.BeginInvokeを呼び出すと、そのスレッドでデリゲートを実行するようにディスパッチャーに指示します スレッドがアイドル状態のとき。
ユニットテストを実行すると、メインスレッドは 決して アイドルになります。すべてのテストを実行して終了します。
このアスペクトユニットをテスト可能にするには、メインスレッドのディスパッチャーを使用しないように、基本となる設計を変更する必要があります。別の選択肢は、 System.ComponentModel.BackgroundWorker 別のスレッドでユーザーを変更します。 (これは単なる例であり、状況によっては不適切な場合があります)。
編集(5か月後)DispatcherFrameを知らないうちにこの回答を書きました。私はこれについて間違っていたことをとても嬉しく思います-DispatcherFrameは非常に便利であることがわかりました。
ユニットテストのセットアップで新しいアプリケーションを作成することにより、この問題を解決しました。
次に、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
}
}
DependencyObject
sへのアクセス時のエラーを回避することが目的の場合は、スレッドとDispatcher
を明示的に操作するのではなく、テストを(単一の)STAThread
スレッド。
これはあなたのニーズに合うかもしれませんし、そうでないかもしれません。少なくとも私にとっては、DependencyObject/WPF関連のテストには常に十分でした。
これを試してみたい場合は、いくつかの方法を紹介します。
[RequiresSTA]
属性があります。ただし、統合テストランナーを使用する場合は注意してください。たとえば、R#4.5 NUnitランナーは古いバージョンのNUnitに基づいているようで、この属性を使用できません。[STAThread]
スレッドを使用するようにNUnitを設定できます。たとえば、Chris Headgateによる このブログ投稿 を参照してください。[STAThread]
スレッドを作成するためのフォールバックメソッド(私は過去に正常に使用しました)があります。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();
}
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);
}
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;
}
}
私は遅れていますが、これは私がそれを行う方法です:
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;
}
私が見つけた最も簡単な方法は、このようなプロパティをDispatcherを使用する必要があるすべてのViewModelに追加することです。
public static Dispatcher Dispatcher => Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
これにより、アプリケーションと単体テストの実行時の両方で機能します。
アプリケーション全体の数か所で使用するだけでよいので、少し繰り返してもかまいません。
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));
}