单元测试时挂在Application.Current.Dispatcher.Invoke(action)上

时间:2014-09-05 06:36:28

标签: c# wpf unit-testing caliburn.micro

我正在为视图模型编写单元测试。该应用程序使用Caliburn.Micro编写,用于MVVM支持。许多视图模型依赖于Application.Current.Dispatcher,有意将一些代码分配到UI线程中。

要从测试中创建Application对象,我编写了以下类:

public class AppInitializer {
    private static Application app;
    public static void InitApp() {
        app = app ?? (app = Application.Current ?? new Application());
    }
}

现在我在每个测试类中执行以下操作:

[ClassInitialize]
    public static void InitClass(TestContext ctx) {
        AppInitializer.InitApp();
    }

不幸的是,从视图模型中第一次调用Application.Current.Dispatcher会挂起我的测试,直到达到超时。

我不想以某种方式抽象Application.CurrentDispatcher,我不想将另一个模拟对象传递给视图模型。如果可能的话,我想得到一些解决方法。

1 个答案:

答案 0 :(得分:3)

我认为你错过了对Application.Run的号召。你是对的,Application类的职责之一是在当前正在执行的线程上创建并启动Dispatcher,但所有这些都发生在调用Run期间。

这就是麻烦开始的地方:Run是一个阻止调用,即在Run退出之前不会执行单元测试。在商店应用中,有一个名为UITestMethod的特殊属性,但我不认为它在WPF中可用(特别是如果您不使用MSTest)。

那你有什么选择?您可以在与运行单元测试的线程不同的线程上创建应用程序 - 但这将导致对Join的方法调用,因为您必须查看是否将调用分派给其他线程。这可能会导致慢速单元测试。

您甚至无法在执行单元测试的线程上手动创建调度程序 - 因为它与前面提到的App类相同:Dispatcher.Run是阻塞调用。

这就是为什么我建议你为Dispatcher创建一个抽象并注入它 - 它可以为你节省很多痛苦。

更新环境背景:

在评论中,我提到了Ambient Context作为一种解决方案,它不依赖于将对象注入符合Dispatcher抽象的视图模型。这就是代码中的样子:

public interface IDispatcher
{
    void ExecuteOnUIThread(Action action);
    // Add whatever methods you need on this interface
}

public static class DispatcherContext
{
    // An instance that implements IDispatcher can be accessed via this static property
    public static IDispatcher Dispatcher { get; set; }
}

// Of course you need to write an adapter for the WPF Dispatcher class

这样,您可以为单元测试创​​建调度程序模拟,但仍然可以通过视图模型中的静态属性访问它。您可以在http://blogs.msdn.com/b/ploeh/archive/2007/07/23/ambientcontext.aspx或Mark Seemann的优秀书籍Dependency Injection in .NET中详细了解此模式。