HowTo-测试WPF应用-NUnit

HowTo-测试WPF应用-NUnit

问题

  1. 积累了不少wpf业务控件,都是通过Demo程序测试和调试功能。
  2. 随着控件的数量和复杂度增加,想通过单元测试来降低复杂度(fix bug后,自动测试bug是否改正,是否引入新bug)

思路

  1. 查看开源项目如何测试的: ControlzEx
  2. 细节如下
    1. 标记测试运行在STA
    2. 所有测试之前实例化 SynchronizationContext,Application
    3. 新增辅助测试的TestWindow DoEvents
    4. 编写单元测试
      1. 加载控件到TestWindow
      2. 修改控件的状态
      3. 调用DoEvents渲染Ui的修改
      4. 对修改后的控件状态进行断言
  3. 所有测试后清理程序

关键代码

标记测试运行在STA

AssemblyInfo.cs

// 程序集的所有测试在STA下运行
[assembly: NUnit.Framework.Apartment(System.Threading.ApartmentState.STA)]

测试环境的设置和清理

AssemblySetup.cs

namespace ControlzEx.Tests
{
    using System.Threading;
    using System.Windows;
    using System.Windows.Threading;
    using NUnit.Framework;

    // 在指定的命名空间的所有fixture之前执行AssemblySetup.Setup, 之后执行AssemblySetup.TearDown
    [SetUpFixture]
    public class AssemblySetup
    {
        [OneTimeSetUp]
        public void Setup()
        {
            // ========知识点==============================
            // SynchronizationContext 
            //  用途: 辅助多线程编程
            //  举例: 线程1获取SynchronizationContext.Current传递给线程2,线程2调用SynchronizationContext.Send方法在线程1执行程序
            //  详情:
            //      只有UI线程才有 SynchronizationContext
            //      WindowsFormsSynchronizationContext Winform,第一次new Form()自动实例化
            //      DispatcherSynchronizationContext Wpf,第一次Application.Run()自动实例化

            // Application 表示运行中的wpf程序
            //  OnExplicitShutdown 除非调用 Application.Shutdown(), 否则应用保持运行
            // ========知识点==============================

            // 手动实例化 SynchronizationContext 用于测试
            SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());

            // 手动实例化 不会自动关闭的Application 用于测试
            _ = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
        }

        // Dispatcher 类
        //      https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.threading.dispatcher?view=windowsdesktop-6.0
        // WPF 的 Application.Current.Dispatcher 中,为什么 Current 可能为 null
        //      https://blog.walterlv.com/post/application-current-may-be-null.html
        // Dispatcher.CurrentDispatcher、Application.Current.Dispatcher与DispatcherObject.Dispatcher的区别
        //      https://www.jianshu.com/p/acdad2533fa8
        // 
        // System.Windows.Threading.Dispatcher 管理线程上的work items的优先级队列的服务
        //  Dispatcher.CurrentDispatcher
        //      获取与当前线程关联的Dispatcher,如果没有就创建一个。
        //  Application.Current.Dispatcher
        //      获取与wpf Ui线程关联的Dispatcher
        //      Application.Current.Dispatcher.Invoke(onPropertyChangedAction); 后台线程要更新Ui,需要将工作委托给Ui的Dispatcher
        //      Application.Current 赋值的时间点, 执行Application的实例构造函数
        //      Application.Current 变为null的时间点, Application.DoShutdown 方法时
        //      Application.Current 抛NullReferenceException的时间点, 非ui线程调用该值可能抛出该异常(ui线程已经开始关闭,但非ui线程还调用Application.Current)
        //      
        //  Dispatcher.InvokeShutdown 关闭线程的work items队列管理服务
        [OneTimeTearDown]
        public void TearDown()
        {
            // 清理&关闭当前ui线程中的work items
            Dispatcher.CurrentDispatcher.InvokeShutdown();
        }
    }
}

辅助类

TestWindow.cs

TestWindow.cs 辅助测试的window不要获取焦点,也不需要显示到任务栏

namespace ControlzEx.Tests.TestClasses
{
    using System;
    using System.Diagnostics;
    using System.Windows;

    public sealed class TestWindow : Window, IDisposable
    {
        public TestWindow()
            : this(null)
        {
        }

        public TestWindow(object content)
        {
            this.Content = content;

            this.Width = 800;
            this.Height = 600;

            // 辅助测试的window不要获取焦点,也不需要显示到任务栏
            this.ShowActivated = false;
            this.ShowInTaskbar = false;

            if (Debugger.IsAttached == false)
            {
                this.Left = int.MinValue;
                this.Top = int.MinValue;
            }

            this.Show();
        }

        public void Dispose()
        {
            this.Close();
        }
    }
}

UITestHelper.cs

UITestHelper.cs 等效于winform的DoEvents方法: Windows Forms 里面的 DoEvents 允许你在执行耗时 UI 操作的过程中插入一段 UI 的渲染过程,使得你的界面看起来并没有停止响应。

namespace ControlzEx.Tests.TestClasses
{
    using System.Windows.Threading;

    public static class UITestHelper
    {
        // Dispatcher.PushFrame 方法      Enters an execute loop
        //      https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.threading.dispatcher.pushframe?redirectedfrom=MSDN&view=windowsdesktop-6.0#System_Windows_Threading_Dispatcher_PushFrame_System_Windows_Threading_DispatcherFrame_
        // 深入了解 WPF Dispatcher 的工作原理(PushFrame 部分)
        //      https://blog.csdn.net/WPwalter/article/details/78093937
        // 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分)
        //      https://blog.csdn.net/WPwalter/article/details/78093917

        // 等效于winform的DoEvents方法: Windows Forms 里面的 DoEvents 允许你在执行耗时 UI 操作的过程中插入一段 UI 的渲染过程,使得你的界面看起来并没有停止响应。、
        // 基础知识:
        //      调用 Dispatcher.PushFrame 可以在不阻塞 UI 线程的情况下等待。
        // 分析:
        //      1. 添加了一个 Background(4) 优先级的 DispatcherOperation,执行的操作就是调用 ExitFrame 方法。(如果不明白这句话,请回过头再看看 Invoke/InvokeAsync 这部分 。)
        //      2. 调用 Dispatcher.PushFrame 以便在不阻塞 UI 线程的情况下等待。
        //      3. 由于用户输入的优先级是 Input(5),UI 响应的优先级是 Loaded(6),渲染的优先级是 Render(7),每一个都比 Background(4)高,于是只要有任何 UI 上的任务,都会先执行,直到没有任务时才会执行 ExiteFrame 方法。(如果不知道为什么,依然请回过头再看看 Invoke/InvokeAsync 这部分 。)
        //      4. 当 ExitFrame 被执行时,它会设置 DispatcherFrame.Continue 为 false。
        //          4.1 DispatcherFrame.Continue 为 false 的目标是让 Dispatcher.PushFrame(frame); 这一句的等待结束,这样才能继续后面代码的执行。
        // 思路:
        //      调用 Dispatcher.PushFrame(frame); 来不阻塞地等待;
        //      设置 frame.Continue = false 来结束等待,继续执行代码。
        /*
                public void DoEvents()
                {
                    DispatcherFrame frame = new DispatcherFrame();
                    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                        new DispatcherOperationCallback(ExitFrame), frame);
                    Dispatcher.PushFrame(frame);
                }

                public object ExitFrame(object f)
                {
                    ((DispatcherFrame)f).Continue = false;
                   
                    return null;
                }
         */

        public static void DoEvents()
        {
            var frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                                                     new DispatcherOperationCallback(
                                                         delegate(object f)
                                                         {
                                                             ((DispatcherFrame)f).Continue = false;
                                                             return null;
                                                         }), frame);
            Dispatcher.PushFrame(frame);
        }
    }
}

具体的单元测试

    [TestFixture]
    public class TabControlExTests
    {
        [Test]
        public void TestAddRemoveInsertWithItemsSource()
        {
            var items = new ObservableCollection<string>
            {
                "1",
                "2",
                "3"
            };

            var tabControl = new TabControlEx
            {
                ItemsSource = items
            };

            using (new TestWindow(tabControl))
            {
                var itemsPanel = (Panel)tabControl.GetType().GetField("itemsHolder", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(tabControl);

                var tabItemsInitial = GetTabItems(tabControl);

                Assert.That(tabItemsInitial, Has.Count.EqualTo(3));

                Assert.That(itemsPanel.Children.Count, Is.EqualTo(1));

                foreach (var tabItem in tabItemsInitial)
                {
                    tabItem.IsSelected = true;
                    UITestHelper.DoEvents();
                }

                Assert.That(itemsPanel.Children.Count, Is.EqualTo(3));

                items.RemoveAt(1);
                items.Insert(1, "2");

                UITestHelper.DoEvents();

                Assert.That(itemsPanel.Children.Count, Is.EqualTo(2));
            }
        }


posted @   colin_xia  阅读(274)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示