WPF UI 测试规范全解析:从 XUnit 单元测试到 FlaUI 集成测试的工程化实践
2026/9/15 16:46:59 网站建设 项目流程

WPF UI 测试规范全解析:从 XUnit 单元测试到 FlaUI 集成测试的工程化实践

【免费下载链接】wpfuiWPF UI provides the Fluent experience in your known and loved WPF framework. Intuitive design, themes, navigation and new immersive controls. All natively and effortlessly.项目地址: https://gitcode.com/GitHub_Trending/wp/wpfui

本篇文章以仓库 docs/architecture/TESTING-SPEC.md 为骨架,系统讲解 WPF UI(wpfui)项目的双测试套件体系:面向逻辑层的 XUnit 单元测试与面向真实界面的 FlaUI 集成测试。你将掌握两套测试的项目结构、框架选型、命名规范、可复用的代码模板与运行命令,并结合仓库源码理解TransitionAnimationProvider等被测对象背后的实现原理。

测试体系总览:为什么 WPF UI 需要两套测试

WPF UI 是一个实现了 Microsoft Fluent Design System 的开源 WPF 控件库,核心库 src/Wpf.Ui 包含 77+ 个自定义控件、主题系统、Win32 互操作、导航服务与动画模块。面对如此庞大的公共 API 面,单一的测试手段显然不够,因此仓库在 docs/architecture/TESTING-SPEC.md 中为开发者和 AI Agent 明确了分层测试策略:

  • 单元测试(Unit Tests):面向纯逻辑与可独立构造的组件,速度快、无 UI 依赖,用于验证动画提供器、扩展方法等内部行为;
  • 集成测试(Integration Tests):直接启动真实的 Gallery 演示应用,通过 UI 自动化驱动控件交互,用于验证窗口标题栏、导航、对话框等端到端场景。

两者以不同框架与模式互补,共同构成 "逻辑正确 + 交互正确" 的双保险。仓库中的实际落地代码位于 tests 目录,与本规范一一对应。

测试项目结构与目标框架

规范明确了两套测试各自独立成项目,互不混用:

维度单元测试集成测试
项目目录tests/Wpf.Ui.UnitTeststests/Wpf.Ui.Gallery.IntegrationTests
目标框架net10.0-windowsnet10.0-windows10.0.26100.0
项目引用Wpf.UiWpf.Ui.FlaUIWpf.Ui.Gallery

从 tests/Wpf.Ui.UnitTests/Wpf.Ui.UnitTests.csproj 与 tests/Wpf.Ui.Gallery.IntegrationTests/Wpf.Ui.Gallery.IntegrationTests.csproj 可以看到更多实现细节:

  • 两个项目均启用ImplicitUsingsOutputTypeExe,并启用UseMicrosoftTestingPlatformRunner/TestingPlatformDotnetTestSupport,即通过 Microsoft Testing Platform 执行测试;
  • 集成测试项目引用..\..\src\Wpf.Ui.FlaUI\Wpf.Ui.FlaUI.csproj(自定义自动化元素包装)与..\..\src\Wpf.Ui.Gallery\Wpf.Ui.Gallery.csproj(被测演示应用);
  • 两个项目都把xunit.runner.jsonContent形式复制到输出目录(CopyToOutputDirectory="PreserveNewest"),保证运行时读取 runner 配置。

需要特别指出的是:集成测试的目标框架net10.0-windows10.0.26100.0直接面向 Windows 10 2024(26100)及更新版本,这与 Gallery 应用依赖的 WinRT/Win32 特性(如系统主题检测、DWM 背景效果)保持一致。

测试框架技术栈

单元测试栈

按规范,单元测试采用以下组合:

  • XUnit v2 风格xunitxunit.runner.visualstudio):断言与测试发现基于 XUnit;
  • NSubstitute 5.3.0:模拟(Mock)框架,用于替身依赖;
  • 标准 XUnit Assert:断言库;
  • Coverlet 6.0.4:代码覆盖率收集器。

一个值得注意的仓库现状:在 Directory.Packages.props(中央包管理清单)中,NSubstitute固定为 5.3.0,与规范一致;而两个测试项目当前实际引用的是xunit.v3(版本 3.2.2),AwesomeAssertions版本为 9.4.0。也就是说,规范描述的 "XUnit v2 风格" 偏历史表述,仓库已经全面迁移到 XUnit v3 与微软测试平台,编写新测试时以当前 csproj 的实际引用为准。

集成测试栈

  • XUnit v3xunit.v3xunit.runner.visualstudio):新一代 XUnit,内置对异步生命周期(IAsyncLifetime)等能力的支持;
  • FlaUI.UIA3 5.0.0:基于 UIA3(UI Automation 3)的自动化框架,负责驱动真实窗口与控件;
  • AwesomeAssertions 9.3.0(仓库实际为 9.4.0):流式断言库,是 FluentAssertions 的继任者,提供Should().Be(...)风格的语法;
  • 自定义 Wpf.Ui.FlaUI:仓库自研的自动化元素包装(如 src/Wpf.Ui.FlaUI/AutoSuggestBox.cs),为 WPF UI 特有控件提供便捷操作。

单元测试模板与最佳实践

命名规范

MethodName_ExpectedResult_WhenCondition

即 "方法名_期望结果_当条件",例如ApplyTransition_ReturnsFalse_WhenDurationIsLessThan10。规范也允许备选格式:

GivenCondition_MethodName_ExpectedResult

例如单元测试扩展方法时使用的GivenAllRegularSymbols_Swap_ReturnsValidFilledSymbol(见 tests/Wpf.Ui.UnitTests/Extensions/SymbolExtensionsTests.cs)。

基本结构与 Arrange/Act/Assert

规范给出了可直接套用的骨架,实测代码位于 tests/Wpf.Ui.UnitTests/Animations/TransitionAnimationProviderTests.cs:

using Xunit; using NSubstitute; using Wpf.Ui.Animations; // Namespace under test namespace Wpf.Ui.UnitTests.Animations; public class TransitionAnimationProviderTests { [Fact] public void ApplyTransition_ReturnsFalse_WhenDurationIsLessThan10() { // Arrange UIElement mockedUiElement = Substitute.For<UIElement>(); // Act var result = TransitionAnimationProvider.ApplyTransition( mockedUiElement, Transition.FadeIn, -10 ); // Assert Assert.False(result); } [Fact] public void ApplyTransition_ReturnsFalse_WhenElementIsNull() { // Arrange UIElement? nullElement = null; // Act var result = TransitionAnimationProvider.ApplyTransition( nullElement, Transition.FadeIn, 100 ); // Assert Assert.False(result); } }

这两个用例恰好印证了被测实现 src/Wpf.Ui/Animations/TransitionAnimationProvider.cs 中的防御式守卫逻辑:ApplyTransition会在duration < 10、元素非UIElement、过渡类型为None、或硬件加速不支持(HardwareAcceleration.IsSupported(RenderingTier.PartialAcceleration)为 false)时直接返回false,同时将时长上限截断到 10000 毫秒(duration > 10000 ? 10000 : duration)。测试与实现一一对应,是典型的 "测试驱动守卫条件" 模式。

使用 NSubstitute 模拟依赖

规范给出了三种核心模拟操作,全部围绕接口替身展开:

// Create mock UIElement element = Substitute.For<UIElement>(); // Setup return value INavigationService service = Substitute.For<INavigationService>(); service.Navigate(typeof(DashboardPage)).Returns(true); // Verify call service.Received(1).Navigate(Arg.Any<Type>());

要点:Substitute.For<T>()创建替身;.Returns(value)编排返回值;Received(1)验证调用次数;Arg.Any<Type>()做参数匹配。注意规范原文引用的是接口风格的INavigationService,实际仓库中该服务接口为 src/Wpf.Ui/INavigationService.cs,编写时可对照真实签名调整。

Global Usings

规范中 "Usings.cs" 的位置在仓库中实际对应 tests/Wpf.Ui.UnitTests/GlobalUsings.cs:

global using System; global using System.Windows; global using NSubstitute; global using Xunit;

借助global using,每个测试文件都不再需要重复using NSubstitute;using Xunit;等语句,从而让测试体聚焦于 Arrange/Act/Assert 本身。集成测试项目则在 csproj 中通过<Using Include="AwesomeAssertions" /><Using Include="NSubstitute" /><Using Include="Xunit" />声明全局导入。

集成测试模板与 UiTest 基类

命名规范

Subject_ShouldExpectedBehavior_WhenCondition

即 "被测对象_应当产生什么行为_当满足什么条件",例如Settings_ShouldBeAvailable_ThroughAutoSuggestBoxCloseButton_ShouldCloseWindow_WhenClicked

基类模式与完整示例

所有集成测试继承自UiTest基类,实际示例见 tests/Wpf.Ui.Gallery.IntegrationTests/NavigationTests.cs:

using AwesomeAssertions; using FlaUI.Core.AutomationElements; using FlaUI.UIA3.Patterns; namespace Wpf.Ui.Gallery.IntegrationTests; public sealed class NavigationTests : UiTest { [Fact] public async Task Settings_ShouldBeAvailable_ThroughAutoSuggestBox() { // Arrange Wpf.Ui.FlaUI.AutoSuggestBox? autoSuggestBox = FindFirst("NavigationAutoSuggestBox")?.As<AutoSuggestBox>(); autoSuggestBox.Should().NotBeNull( "because AutoSuggestBox should be present in the navigation bar" ); // Act autoSuggestBox!.Enter("Settings"); await Wait(1); // Assert TextBox? pageTitle = FindFirst("PageTitle")?.AsTextBox(); pageTitle.Should().NotBeNull(); pageTitle!.Text.Should().Be("Settings"); } }

这个用例展示了集成测试的完整链路:通过 AutomationId 定位控件 → 流式断言确认存在 → 调用Enter输入文本 → 等待 UI 刷新 → 再次定位并断言结果。其中Wpf.Ui.FlaUI.AutoSuggestBox是仓库为 AutoSuggestBox 定制的包装(见 src/Wpf.Ui.FlaUI/AutoSuggestBox.cs),其Enter方法内部执行:点击元素 → 清空 Value 模式的值 → 用Keyboard.Type逐字符输入 → 按回车触发查询 → 等待输入被处理(Wait.UntilInputIsProcessed())。这正是纯TextBox.Text赋值无法模拟的完整用户输入事件流。

UiTest 基类能力详解

基类位于 tests/Wpf.Ui.Gallery.IntegrationTests/Fixtures/UiTest.cs,对外暴露以下方法:

// Find element by automation ID protected AutomationElement? FindFirst(string automationId) // Find element by condition protected AutomationElement? FindFirst(Func<ConditionFactory, ConditionBase> buildCondition) // Wait for specified seconds protected async Task Wait(int seconds, CancellationToken cancellationToken = default) // Type text protected void Enter(string text) // Press key protected void Press(VirtualKeyShort key)

生命周期UiTest实现了IAsyncLifetime,每个测试都会获得一个独立的应用实例——这得益于TestedApplicationfixture(tests/Wpf.Ui.Gallery.IntegrationTests/Fixtures/TestedApplication.cs):

  • InitializeAsync:定位输出目录下的Wpf.Ui.Gallery.exe,若不存在直接抛出InvalidOperationException;随后通过Application.Launch(path)启动应用,并调用WaitWhileMainHandleIsMissing(TimeSpan.FromMinutes(1))等待主窗口句柄出现(最多 1 分钟);
  • DisposeAsync:先Close()关闭应用,再用Retry.WhileFalse(...)重试确认进程退出(2 秒超时),最后释放UIA3Automation

也就是说,"自动清理状态" 由 fixture 兜底,测试自身无需管理进程生命周期。

EnterPress的底层都基于 FlaUI 的Keyboard.Type,并在输入后调用Wait.UntilInputIsProcessed()等待输入被系统消化;Enter还支持多行文本(以\r\n/\n拆分后逐行输入并穿插回车键)。

AwesomeAssertions 流式断言语法

规范整理了几类高频断言,可直接用于验证控件状态:

// Null checks element.Should().NotBeNull("because element must exist"); element.Should().BeNull(); // String assertions text.Should().Be("Expected"); text.Should().Contain("substring"); text.Should().StartWith("prefix"); // Boolean assertions condition.Should().BeTrue("because condition must be met"); Application?.HasExited.Should().BeTrue(); // Collection assertions items.Should().HaveCount(5); items.Should().Contain(item);

注意每个断言都鼓励携带 "because" 说明原因,失败时输出更可读的诊断信息。集成测试对断言的依赖也体现在 csproj 的<Using Include="AwesomeAssertions" />中,整个项目无需显式写 using。

FlaUI 元素访问与控件交互

// Find and cast to specific control Button? button = FindFirst("ButtonId").AsButton(); TextBox? textBox = FindFirst("TextBoxId")?.AsTextBox(); // Custom automation elements var autoSuggestBox = FindFirst("AutoSuggestBoxId")?.AsAutoSuggestBox(); // Interact with controls button.Click(moveMouse: false); textBox.Text = "value"; // Pattern-based interaction var invokePattern = element.Patterns.Invoke.Pattern; invokePattern.Invoke();

AsButton()AsTextBox()等是 FlaUI 的类型转换扩展;Click(moveMouse: false)表示不移动真实鼠标、直接触发点击,适合后台自动化;对于不支持便捷方法的控件,可直接走 UIA 模式(如Invoke模式)操作。

运行测试:命令行实战

单元测试

# Run all unit tests dotnet test tests/Wpf.Ui.UnitTests/ # Run specific test class dotnet test tests/Wpf.Ui.UnitTests/ --filter "FullyQualifiedName~TransitionAnimationProviderTests" # Run with coverage dotnet test tests/Wpf.Ui.UnitTests/ --collect:"XPlat Code Coverage"
  • --filter "FullyQualifiedName~XXX"使用子串匹配完整限定名,可精确到类甚至方法;
  • --collect:"XPlat Code Coverage"需要 Coverlet collector 支持(规范标注版本 6.0.4),产出覆盖率报告数据。

集成测试

# Run all integration tests dotnet test tests/Wpf.Ui.Gallery.IntegrationTests/ # Run specific test dotnet test tests/Wpf.Ui.Gallery.IntegrationTests/ --filter "FullyQualifiedName~TitleBarTests" # Run with diagnostics dotnet test tests/Wpf.Ui.Gallery.IntegrationTests/ --logger "console;verbosity=detailed"

集成测试会真实启动Wpf.Ui.Gallery.exe并驱动 UI,因此执行环境必须是 Windows(且支持 UIA3),耗时远高于单元测试;建议用--filter先跑单个测试类(如TitleBarTests)验证环境,再跑全量。

xunit.runner.json 运行器配置

配置文件位于 tests/Wpf.Ui.Gallery.IntegrationTests/xunit.runner.json:

{ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", "parallelizeTestCollections": false, "diagnosticMessages": true, "culture": "invariant" }

三项配置各有深意:

  • parallelizeTestCollections: false集成测试绝不并行。因为所有用例共享同一个 Gallery 应用实例(单进程),并行会导致窗口状态互相干扰;
  • diagnosticMessages: true:输出诊断消息,便于排查自动化启动/查找失败;
  • culture: "invariant":使用固定区域性,避免界面文案与断言因区域设置不同而失败。

测试组织规范

命名空间镜像

测试命名空间严格镜像源码命名空间,方便在源码与测试之间跳转:

src/Wpf.Ui/Animations/TransitionAnimationProvider.cs ↓ tests/Wpf.Ui.UnitTests/Animations/TransitionAnimationProviderTests.cs

文件命名遵循{ClassName}Tests.csTransitionAnimationProvider.csTransitionAnimationProviderTests.csSymbolExtensions.csSymbolExtensionsTests.cs

当前覆盖范围

规范记录了截至编写时的测试覆盖(也是仓库 tests 目录的真实状态):

单元测试覆盖:

  • 动画:TransitionAnimationProvider(守卫条件验证);
  • 扩展:SymbolExtensions.Swap()SymbolExtensions.GetString()。后者在 tests/Wpf.Ui.UnitTests/Extensions/SymbolExtensionsTests.cs 中通过遍历SymbolRegular/SymbolFilled全部枚举值来保证每个图标枚举都能转换为有效字符(Empty除外),属于数据驱动式穷举测试。

集成测试覆盖:

  • 窗口标题验证;
  • TitleBar 按钮交互(关闭、最小化、最大化);
  • 通过 AutoSuggestBox 导航;
  • 通过 NavigationView 导航;
  • ContentDialog 结果验证;
  • ContentDialog 键盘焦点隔离。

以 tests/Wpf.Ui.Gallery.IntegrationTests/TitleBarTests.cs 为例,MaximizeButton_ShouldExpandWindow_WhenClicked点击TitleBarMaximizeButton后,通过MainWindow.Patterns.Window.Pattern.WindowVisualState.ValueOrDefault断言窗口进入WindowVisualState.Maximized状态——这是对 WPF UI 自定义窗口镶边(FluentWindow/TitleBar)最直接的端到端验证。

面向 AI Agent 的编写指南

规范专门面向 AI Agent 编写测试的场景给出了纪律性要求:

编写单元测试时

  1. 用 NSubstitute 模拟依赖,不要构造真实的重型对象;
  2. 只测试公共 API 表面,不测私有实现细节;
  3. 使用 XUnit Assert 方法断言;
  4. 严格遵守命名规范
  5. 每个测试逻辑上只断言一件事
  6. 成功与失败路径都要覆盖——正如TransitionAnimationProviderTests同时覆盖了负时长与空元素两个失败分支。

编写集成测试时

  1. 继承UiTest基类,复用应用生命周期管理;
  2. 使用 AutomationId 定位元素(如NavigationAutoSuggestBoxTitleBarCloseButton),而非依赖坐标;
  3. Wait()给 UI 更新留出时间(通常 1~2 秒);
  4. 使用 AwesomeAssertions 流式语法
  5. 为断言提供清晰的 "because" 说明
  6. 状态清理交给 fixture,测试自身不重复处理。

可参考的模板文件

  • 单元测试模板:tests/Wpf.Ui.UnitTests/Animations/TransitionAnimationProviderTests.cs、tests/Wpf.Ui.UnitTests/Extensions/SymbolExtensionsTests.cs;
  • 集成测试模板:tests/Wpf.Ui.Gallery.IntegrationTests/TitleBarTests.cs、tests/Wpf.Ui.Gallery.IntegrationTests/NavigationTests.cs。

常见测试模式

测试依赖属性(Dependency Property)

WPF 控件大量使用依赖属性,测试应同时验证默认值与可读写性:

[Fact] public void PropertyName_DefaultValue_IsExpected() { var control = new MyControl(); Assert.Equal(expectedDefault, control.PropertyName); } [Fact] public void PropertyName_CanBeSet_AndRetrieved() { var control = new MyControl(); var expectedValue = new SomeType(); control.PropertyName = expectedValue; Assert.Equal(expectedValue, control.PropertyName); }

测试服务(Service)

服务类依赖接口时,用 NSubstitute 构造完整的替身链:

[Fact] public void Navigate_ReturnsTrue_WhenNavigationSucceeds() { // Arrange var pageProvider = Substitute.For<INavigationViewPageProvider>(); pageProvider.GetPage(Arg.Any<Type>()).Returns(new DashboardPage()); var service = new NavigationService(pageProvider); var navigationView = Substitute.For<INavigationView>(); service.SetNavigationControl(navigationView); // Act bool result = service.Navigate(typeof(DashboardPage)); // Assert Assert.True(result); }

这里INavigationViewPageProvider是 src/Wpf.Ui.Abstractions/INavigationViewPageProvider.cs 中的契约接口,NavigationService的对应实现位于 src/Wpf.Ui/NavigationService.cs,可对照真实签名编写。

持续集成现状与扩展建议

规范明确指出:当前测试并未接入 CI,PR 校验器只负责构建 Gallery 应用。若要将测试执行纳入 CI,需在.github/workflows/wpf-ui-pr-validator.yaml中追加如下步骤:

- name: Run Unit Tests run: dotnet test tests/Wpf.Ui.UnitTests/ --no-restore --verbosity normal - name: Run Integration Tests run: dotnet test tests/Wpf.Ui.Gallery.IntegrationTests/ --no-restore --verbosity normal

需要提醒的是:集成测试依赖 Windows 桌面会话与 UIA3,若 CI 使用 Linux 容器则无法运行,应将其限定在windows-latest的 runner 上,并考虑是否需要 headless 会话支持。这也是集成测试目前留在本地执行的原因之一。

小结

WPF UI 的测试规范为开发者与 AI Agent 提供了一套完整、可复制的工程化测试框架:单元测试以 XUnit + NSubstitute 守住逻辑正确性,集成测试以 XUnit v3 + FlaUI.UIA3 + AwesomeAssertions 守住交互正确性;命名规范、基类设计、fixture 生命周期与运行器配置环环相扣。深入阅读 docs/architecture/TESTING-SPEC.md 并对照 tests 目录下的真实用例,即可快速上手为 WPF UI 及其衍生应用编写高质量测试。

【免费下载链接】wpfuiWPF UI provides the Fluent experience in your known and loved WPF framework. Intuitive design, themes, navigation and new immersive controls. All natively and effortlessly.项目地址: https://gitcode.com/GitHub_Trending/wp/wpfui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询