【问题标题】:Why does this unit test pass on my machine but fail on the build server?为什么这个单元测试在我的机器上通过但在构建服务器上失败?
【发布时间】:2012-10-16 18:08:41
【问题描述】:

我正在使用 VS2010,用 MSTest 编写单元测试。我的项目使用 WPF、MVVM 和 PRISM 框架。我也在使用 Moq 来模拟接口。

我正在测试命令和列表中选定项目之间的交互。根据 MVVM 模式,交互被封装在 ViewModel 中。基本上,当设置了 SelectedDatabase 时,我希望命令引发 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();
        }
    }

还有在视图模型上:

    bool OpenDatabaseCanExecute()
    {
        return _selectedDatabase != null;
    }

TestHelpers.HelpGetMockIDatabase() 只是得到一个模拟 IDatabase 并设置了一些属性。

当我从 VS2010 运行测试时,此测试通过,但在服务器上作为自动构建的一部分执行时失败。我输入了AutoResetEvent 尝试解决问题,但没有任何效果。

我发现自动化测试在 MSTest 命令行中使用了 noisolation 标志,所以我删除了它。然而,这产生了一次“通过”,但下一次产生了“失败”。

我认为我在这一切中遗漏了一些重要的东西,但我无法弄清楚它是什么。谁能帮忙告诉我我做错了什么?

【问题讨论】:

    标签: c# wpf unit-testing mvvm mstest


    【解决方案1】:

    更新

    您的代码可能会失败的唯一其他剩余位置是在您的 sn-p 中的这两行中的 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 或您的构建服务器隔离开来。

    原创

    这个sn-p 的代码看起来很可疑。如果您的事件从另一个线程触发,则原始线程可能会在您分配之前先退出等待,导致您的标志被读取为陈旧值。

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

    【讨论】:

    • 感谢您指出我在订购时的错误。我应该已经发现了。但是,我尝试过调整顺序,但没有效果。处理 WaitHandle.WaitOne 的返回码表明没有调用“resetEvent.Set()”。不幸的是,测试仍然失败。
    • 好的。找到另一个可能有帮助的参考资料;现在更新这个答案。
    • 10 次这样的测试事件中有 9 次由于构建服务器上的资源负载而失败。大多数情况下,增加超时时间并确保事情的顺序正确可以解决问题。
    • @bryanbcook - 我尝试将超时时间增加到 10 秒,但没有成功。
    • @meklarian 感谢您的参考。实际上,在 PRISM 中,NotificationObject 抽象基类有一个方法可以执行您建议的删除魔术字符串依赖项的方法。我将更新我的代码以使用它。关于您的其他建议,我想我可能已经找到了另一种解释和解决方案。如果结果有成果,我会尽快更新。
    【解决方案2】:

    我找到了一个答案来解释这个单元测试发生了什么。当时还有其他一些我没有意识到的复杂因素很重要。我没有在最初的问题中包含这些细节,因为我认为它们不相关。

    代码问题中描述的视图模型是使用与 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 运行的。

    【讨论】:

      猜你喜欢
      • 2012-12-13
      • 2021-03-28
      • 2011-10-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-08
      • 1970-01-01
      • 2011-10-09
      相关资源
      最近更新 更多