【问题标题】:Launch and call methods on WPF app from unit test project从单元测试项目中启动和调用 WPF 应用程序上的方法
【发布时间】:2019-11-21 19:19:09
【问题描述】:

我想启动一个 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 应用程序。想法?

【问题讨论】:

  • 我确信这是可能的,但我感觉无论你推出什么解决方案都会像 CodedUI 一样复杂。考虑以下用例:如何在 StackPanel 中获取 TextBox 的值,该 StackPanel 位于 Dialog 中托管的 Grid 中?现在您将如何做到这一点,以使您的解决方案不会脆弱(例如,在不破坏测试的情况下删除网格)。
  • @Ilian:按照我阅读问题的方式,OP 正在寻求直接操作底层视图模型对象。 IE。他不想尝试与 UI 交互,而是想绕过 UI,只确保 视图模型到 UI 的绑定之类的东西正常工作。在这种情况下,我认为“工作”只是意味着“没有例外”。如果需要检查 UI 对象属性,则可以通过其他方式对其进行抽象。这方面的细节将是一个完整的“另一个问题”。 :)
  • @PeterDuniho 这很有道理:)
  • 你们都提出了很好的问题。我们需要完全重写 ViewModel 和服务层以与完全不同的后端进行通信。诸如 CodedUI 之类的解决方案往往会导致缓慢、不稳定的测试,并且不会减少我们将要进行的测试数量。单元测试也无济于事。

标签: c# wpf multithreading integration-testing


【解决方案1】:

您需要一个单独的线程来操作视图模型,或者您需要在调度程序线程中执行代码来执行此操作。我更喜欢后者,但两者都行。前者需要您小心使用调度程序将一些操作编组到 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 类具有一个Text 属性,并且主窗口的DataContext 属性设置为这个ViewModel 的一个实例。在我的示例程序中,我只是将Text 属性绑定到TextBlock.Text 属性。显然,您可以对视图模型做任何您想做的事情。

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

    【讨论】:

    • 非常感谢 mm8,但是我们无法对代码进行任何实质性的重构,而且已经有数百个单元测试无法保护我们免受即将发生的更改的影响。我们必须完全重写服务层,视图模型也会受到影响。我们需要对此进行覆盖范围非常广泛的全面集成测试。
    猜你喜欢
    • 2011-01-23
    • 2014-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-15
    • 1970-01-01
    相关资源
    最近更新 更多