从单元测试项目启动和调用 WPF 应用程序的方法

Launch and call methods on WPF app from unit test project

我想启动一个 WPF 应用程序并调用 ViewModel 上的方法来控制该应用程序以进行集成测试。类似于:

    [Test]
    public void Test1()
        var application = new MyApp();
        application.InitializeComponent();
        application.Run();

(好的,这会在此时停止测试执行,大概是将控制权传递给 WPF 应用程序。不确定如何处理这个问题。在单独的线程中启动或其他什么?)

然后我希望能够在 ViewModel 上获取和设置值,如下所示:

        application.MyViewModel.SomeProperty = "A value!";

这里的目标是在不求助于 WinAppDriver、White、CodedUI 或任何类似的混乱的情况下,以集成的方式测试 WPF 应用程序。想法?

您需要一个单独的线程来操作视图模型,或者您需要在调度程序线程中执行代码来执行此操作。我更喜欢后者,但两者都行。前者要求您谨慎使用调度程序将某些操作编组到 UI 线程;视图模型 属性 更改不需要那个,因为 WPF 会自动为您执行此操作,但其他事情如直接调用 UI 对象方法——例如Window.Close() — 做。

下面是您可以使用调度程序线程执行所有测试代码的示例:

[TestMethod]
public void TestWpfApp()
{
    Thread thread = new Thread(() =>
    {
        var application = new App();
        Application.ResourceAssembly = System.Reflection.Assembly.GetExecutingAssembly();
        application.InitializeComponent();
        application.Dispatcher.InvokeAsync(() =>
        {
            _TestApplication(application);
        }, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
        application.Run();
    });

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    thread.Join();
}

private static async void _TestApplication(Application application)
{
    Window window = application.MainWindow;
    ViewModel viewModel = (ViewModel)window.DataContext;

    await Task.Delay(TimeSpan.FromSeconds(5));
    viewModel.Text = "Hello World!";
    await Task.Delay(TimeSpan.FromSeconds(5));
    window.Close();
}

基本结构是设置一个适合运行WPF的线程UI(必须是STA线程,不要乱用单元测试的线程,因此需要为此目的创建一个新线程),然后在该线程中,通过 InvokeAsync() 对调度程序进行通常的 WPF 设置和队列调用主要测试方法,使其在 WPF 启动后开始执行代码已经开始 运行.

当然,这个例子假设 ViewModel class 和 Text 属性,以及主要 window 的 DataContext 属性 设置为此 ViewModel 的一个实例。在我的示例程序中,我只是将 Text 属性 绑定到 TextBlock.Text 属性。显然,您可以对视图模型做任何您想做的事情。

请注意,我必须明确设置 Application.ResourceAssembly。在我目前正在使用的 Visual Studio Community 2017 中,单元测试框架在 Assembly.GetEntryAssembly() returns null 的上下文中运行文本,这打破了 WPF 的资源加载。设置它明确修复了这个问题(我正在使用 Assembly.GetExecutingAssembly(),因为我将单元测试代码与我的示例 WPF 程序放在同一个程序集中......显然,如果你将它们放在不同的程序集中,你必须找到以其他方式正确组装)。

在我的测试中,在对 Dispatch.InvokeAsync() 的调用中使用 System.Windows.Threading.DispatcherPriority.ApplicationIdle 并不是严格要求的。我发现 MainWindowDataContext 属性初始化良好。但我更喜欢显式等待 ApplicationIdle,只是为了确保它们已完全初始化,并且 WPF 程序本身已准备好开始接受您为测试准备的任何输入。

在单元测试或集成测试中创建 App 并对其调用 Run() 没有多大意义。每个 AppDomain.

只能创建一个 App

您应该做的是以某种方式构建您的代码,这样您就不必单独测试 ViewModel class 之外的任何其他内容。这意味着您的所有应用程序逻辑都应该在 ViewModel 中实现,或者在 ViewModel 使用一种或另一种方式的 classes 中实现。

这是一种称为 Model-View-ViewModel (MVVM) 的设计模式,它是 开发时推荐使用的设计模式 XAML-based UI应用程序。这是有原因的——测试就是其中之一。

如果你搜索一下,你会发现很多关于 MVVM 的信息。 This 应该是一个很好的起点。