为什么这个单元测试在我的机器上传递但在构建服务器上失败?

时间:2012-10-16 18:08:42

标签: c# wpf unit-testing mvvm mstest

我正在使用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标志,所以我删除了它。然而,这产生了一次“通过”,但是下一次“失败”。

我认为我遗漏了所有这些重要的东西,但我无法弄清楚它是什么。任何人都可以通过告诉我我做错了什么来帮忙吗?

2 个答案:

答案 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运行的。