我正在使用VS2010,使用MSTest编写单元测试。我的项目使用WPF,MVVM和PRISM框架。我也使用Moq来模拟接口。
我正在测试命令与列表中所选项目之间的交互。根据MVVM模式将交互封装在ViewModel中。基本上,当设置SelectedDatabase时,我希望Command引发CanExecute。我已经为这种行为写了这个测试:
public void Test()
{
var databaseService = new Mock<IDatabaseService>();
var databaseFunctionsController = new Mock<IDatabaseFunctionsController>();
// Create the view model
OpenDatabaseViewModel viewModel
= new OpenDatabaseViewModel(databaseService.Object, databaseFunctionsController.Object);
// Mock up the database and its view model
var database = TestHelpers.HelpGetMockIDatabase();
var databaseViewModel = new DatabaseViewModel(database.Object);
// Hook up the can execute changed event
var resetEvent = new AutoResetEvent(false);
bool canExecuteChanged = false;
viewModel.OpenDatabaseCommand.CanExecuteChanged += (s, e) =>
{
resetEvent.Set();
canExecuteChanged = true;
};
// Set the selected database
viewModel.SelectedDatabase = databaseViewModel;
// Allow the event to happen
resetEvent.WaitOne(250);
// Check that it worked
Assert.IsTrue(canExecuteChanged,
"OpenDatabaseCommand.CanExecuteChanged should be raised when SelectedDatabase is set");
}
在OpenDatabaseViewModel
上,SelectDatabase
属性如下:
public DatabaseViewModel SelectedDatabase
{
get { return _selectedDatabase; }
set
{
_selectedDatabase = value;
RaisePropertyChanged("SelectedDatabase");
// Update the can execute flag based on the save
((DelegateCommand)OpenDatabaseCommand).RaiseCanExecuteChanged();
}
}
还有viewmodel:
bool OpenDatabaseCanExecute()
{
return _selectedDatabase != null;
}
TestHelpers.HelpGetMockIDatabase()
只会获得一个设置了一些属性的模拟IDatabase
。
当我从VS2010运行测试时,此测试通过,但在作为服务器上的自动构建的一部分执行时失败。我放入AutoResetEvent
试图解决问题,但它没有效果。
我发现自动化测试在MSTest命令行中使用noisolation
标志,所以我删除了它。然而,这产生了一次“通过”,但是下一次“失败”。
我认为我遗漏了所有这些重要的东西,但我无法弄清楚它是什么。任何人都可以通过告诉我我做错了什么来帮忙吗?
答案 0 :(得分:1)
代码可能失败的唯一其他地方是SelectedDatabase
属性的代码段中的这两行。
RaisePropertyChanged("SelectedDatabase");
// Update the can execute flag based on the save
((DelegateCommand)OpenDatabaseCommand).RaiseCanExecuteChanged();
还有其他人在使用RaisePropertyChanged()
时遇到了一些问题,并使用了魔法字符串;但这可能不是你眼前的问题。尽管如此,如果您想要消除魔术字符串依赖关系的路径,您可以查看这些链接。
WPF, MVVM, and RaisePropertyChanged @ WilberBeast
MVVM - RaisePropertyChanged turning code into a mess
RaiseCanExecuteChanged()
方法是另一个嫌疑人,在PRISM中查找文档显示此方法需要在UI线程上调度事件。从mstest开始,无法保证使用UI线程来分派测试。
DelegateCommandBase.RaiseCanExecuteChanged @ MSDN
我建议你在它周围添加一个try / catch块,看看在调用RaiseCanExecuteChanged()
时是否抛出任何异常。请注意抛出的异常,以便您可以决定下一步如何继续。如果您绝对需要测试此事件调度,您可以考虑编写一个小型WPF感知应用程序(或者可能是STAThread控制台应用程序)来运行实际测试并退出,并让您的测试启动该应用程序来观察结果。这会将您的测试与任何threading concerns that could be caused by mstest或您的构建服务器隔离开来。
这段代码似乎令人怀疑。如果您的事件从另一个线程触发,原始线程可能会在您的分配之前首先退出等待,从而导致您的标志被读取为陈旧值。
viewModel.OpenDatabaseCommand.CanExecuteChanged += (s, e) =>
{
resetEvent.Set();
canExecuteChanged = true;
};
考虑将块中的行重新排序为:
viewModel.OpenDatabaseCommand.CanExecuteChanged += (s, e) =>
{
canExecuteChanged = true;
resetEvent.Set();
};
另一个问题是你不检查你的等待是否满足。如果在没有信号的情况下经过了250毫秒,那么你的旗帜将是假的。
请参阅WaitHandle.WaitOne以查看您将收到的返回值,并更新此部分代码以处理无信号退出的情况。
// Allow the event to happen
resetEvent.WaitOne(250);
// Check that it worked
Assert.IsTrue(canExecuteChanged,
"OpenDatabaseCommand.CanExecuteChanged should be raised when SelectedDatabase is set");
答案 1 :(得分:1)
我找到了一个答案来解释这个单元测试的情况。还有其他一些复杂的因素,我当时没有意识到这些因素很重要。我没有在原始问题中包含这些细节,因为我认为它们不相关。
代码问题中描述的视图模型是使用与WinForms集成的项目的一部分。我正在托管一个PRISM shell作为ElementHost的孩子。关于stackoverflow How to use Prism within an ElementHost上的问题的答案,添加此内容以创建适当的Application.Current
:
public class MyApp : System.Windows.Application
{
}
if (System.Windows.Application.Current == null)
{
// create the Application object
new MyApp();
}
上述代码不受相关单元测试的影响。但是,它正在预先运行的其他单元测试中运行,并且所有单元测试都使用带有MSTest.exe的/noisolation
标志一起运行。
这为什么重要?好吧,埋没在PRISM代码中,该代码被称为
的结果((DelegateCommand)OpenDatabaseCommand).RaiseCanExecuteChanged();
内部类Microsoft.Practices.Prism.Commands.WeakEventHandler
中的是这种方法:
public static DispatcherProxy CreateDispatcher()
{
DispatcherProxy proxy = null;
#if SILVERLIGHT
if (Deployment.Current == null)
return null;
proxy = new DispatcherProxy(Deployment.Current.Dispatcher);
#else
if (Application.Current == null)
return null;
proxy = new DispatcherProxy(Application.Current.Dispatcher);
#endif
return proxy;
}
然后使用调度程序调用有问题的事件处理程序:
private static void CallHandler(object sender, EventHandler eventHandler)
{
DispatcherProxy dispatcher = DispatcherProxy.CreateDispatcher();
if (eventHandler != null)
{
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke((Action<object, EventHandler>)CallHandler, sender, eventHandler);
}
else
{
eventHandler(sender, EventArgs.Empty);
}
}
}
因此,它会尝试在当前应用程序的UI线程上调度该事件(如果有)。否则它只是调用eventHandler。对于有问题的单元测试,这导致事件丢失。
在尝试了很多不同的事情之后,我解决的解决方案就是将单元测试分成不同的批次,所以上面的单元测试是用Application.Current == null
运行的。