我无法让Dispatcher运行委托我在单元测试时传递给它。当我运行程序时,一切正常,但是,在单元测试期间,以下代码将无法运行:
this.Dispatcher.BeginInvoke(new ThreadStart(delegate
{
this.Users.Clear();
foreach (User user in e.Results)
{
this.Users.Add(user);
}
}), DispatcherPriority.Normal, null);
我在我的viewmodel基类中有这个代码来获取Dispatcher:
if (Application.Current != null)
{
this.Dispatcher = Application.Current.Dispatcher;
}
else
{
this.Dispatcher = Dispatcher.CurrentDispatcher;
}
我是否需要做一些事情来初始化Dispatcher进行单元测试? Dispatcher永远不会在委托中运行代码。
答案 0 :(得分:84)
通过使用Visual Studio单元测试框架,您无需自己初始化Dispatcher。你完全正确,Dispatcher不会自动处理它的队列。
您可以编写一个简单的帮助方法“DispatcherUtil.DoEvents()”,它告诉Dispatcher处理其队列。
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) 中找到此课程。
答案 1 :(得分:21)
我们通过简单地模拟接口后面的调度程序并从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 :(得分:16)
您可以使用调度程序进行单元测试,只需使用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。
答案 3 :(得分:2)
当您调用Dispatcher.BeginInvoke时,您指示调度程序在线程空闲时在其线程上运行委托。
运行单元测试时,主线程将从不空闲。它将运行所有测试然后终止。
要使此方面单元可测试,您必须更改基础设计,以便它不使用主线程的调度程序。另一种方法是利用 System.ComponentModel.BackgroundWorker 来修改不同线程上的用户。 (这只是一个例子,根据具体情况可能不合适。)
修改(5个月后) 我在不知道DispatcherFrame时写了这个答案。我很高兴在这个问题上出错了 - DispatcherFrame已经证明是非常有用的。
答案 4 :(得分: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);
}
答案 5 :(得分: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。
答案 6 :(得分:2)
我通过在单元测试设置中创建一个新的应用程序来解决这个问题。
然后,任何访问Application.Current.Dispatcher的测试类都会找到一个调度程序。
因为AppDomain中只允许一个Application,所以我使用了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
}
}
答案 7 :(得分:1)
如果您的目标是在访问DependencyObject
时避免错误,我建议您不要明确地使用线程和Dispatcher
,而只需确保您的测试以(单个)运行{ {1}}线程。
这可能适合您的需求,也可能不适合您,至少对于测试与DependencyObject / WPF相关的内容来说,它总是足够的。
如果你想尝试这个,我可以指出几种方法:
STAThread
属性来定位测试方法或类。请注意,如果您使用集成的测试运行器,例如R#4.5 NUnit运行器似乎基于旧版本的NUnit,并且不能使用此属性。[RequiresSTA]
线程,例如参见Chris Headgate的this blog post。[STAThread]
线程来运行测试。答案 8 :(得分:0)
我在MVVM范例中使用MSTest
和Windows Forms
技术。
在尝试了很多解决方案后,这个(found on Vincent Grondin blog)对我有用:
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));
}
答案 9 :(得分:0)
我建议在DispatcherUtil中再添加一个方法,将其调用DoEventsSync(),并调用Dispatcher调用而不是BeginInvoke。如果您真的必须等到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;
}
}
答案 10 :(得分:0)
我通过在自己的IDispatcher接口中包装Dispatcher,然后使用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);
}
答案 11 :(得分: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();
}
答案 12 :(得分:0)
我来晚了,但这是我的方法:
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;
}
答案 13 :(得分:0)
我发现的最简单的方法是向需要使用Dispatcher的任何ViewModel添加这样的属性:
public static Dispatcher Dispatcher => Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
这样,它既可以在应用程序中运行,也可以在运行单元测试时运行。
我只需要在整个应用程序中的几个地方使用它,所以我不介意重复自己。
答案 14 :(得分:0)
Winforms有一个非常简单且与WPF兼容的解决方案。
从单元测试项目中,引用System.Windows.Forms。
在单元测试中,当您要等待调度程序事件完成处理时,请致电
System.Windows.Forms.Application.DoEvents();
如果您有一个后台线程不断将Invokes添加到调度程序队列中,那么您将需要进行某种测试并继续调用DoEvents,直到满足其他一些可测试条件为止
while (vm.IsBusy)
{
System.Windows.Forms.Application.DoEvents();
}
答案 15 :(得分:0)
这是一个有点过时的帖子,BeginInvoke在今天并不是首选。 我一直在寻找模拟解决方案,却没有为InvokeAsync找到任何东西:
await App.Current.Dispatcher.InvokeAsync(() => something );
我添加了一个名为Dispatcher的新类,实现了IDispatcher,然后将其注入到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);