ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

WPF UI 测试规范全解析:从 XUnit 单元测试到 FlaUI 集成测试的工程化实践 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 UIwpfui项目的双测试套件体系面向逻辑层的 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.FlaUI、Wpf.Ui.Gallery从 tests/Wpf.Ui.UnitTests/Wpf.Ui.UnitTests.csproj 与 tests/Wpf.Ui.Gallery.IntegrationTests/Wpf.Ui.Gallery.IntegrationTests.csproj 可以看到更多实现细节两个项目均启用ImplicitUsingsOutputType为Exe并启用UseMicrosoftTestingPlatformRunner/TestingPlatformDotnetTestSupport即通过 Microsoft Testing Platform 执行测试集成测试项目引用..\..\src\Wpf.Ui.FlaUI\Wpf.Ui.FlaUI.csproj自定义自动化元素包装与..\..\src\Wpf.Ui.Gallery\Wpf.Ui.Gallery.csproj被测演示应用两个项目都把xunit.runner.json以Content形式复制到输出目录CopyToOutputDirectoryPreserveNewest保证运行时读取 runner 配置。需要特别指出的是集成测试的目标框架net10.0-windows10.0.26100.0直接面向 Windows 10 202426100及更新版本这与 Gallery 应用依赖的 WinRT/Win32 特性如系统主题检测、DWM 背景效果保持一致。测试框架技术栈单元测试栈按规范单元测试采用以下组合XUnit v2 风格xunit、xunit.runner.visualstudio断言与测试发现基于 XUnitNSubstitute 5.3.0模拟Mock框架用于替身依赖标准 XUnit Assert断言库Coverlet 6.0.4代码覆盖率收集器。一个值得注意的仓库现状在 Directory.Packages.props中央包管理清单中NSubstitute固定为 5.3.0与规范一致而两个测试项目当前实际引用的是xunit.v3版本 3.2.2AwesomeAssertions版本为 9.4.0。也就是说规范描述的 XUnit v2 风格 偏历史表述仓库已经全面迁移到 XUnit v3 与微软测试平台编写新测试时以当前 csproj 的实际引用为准。集成测试栈XUnit v3xunit.v3、xunit.runner.visualstudio新一代 XUnit内置对异步生命周期IAsyncLifetime等能力的支持FlaUI.UIA3 5.0.0基于 UIA3UI 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.csusing 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.ForUIElement(); // 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.ForUIElement(); // Setup return value INavigationService service Substitute.ForINavigationService(); service.Navigate(typeof(DashboardPage)).Returns(true); // Verify call service.Received(1).Navigate(Arg.AnyType());要点Substitute.ForT()创建替身.Returns(value)编排返回值Received(1)验证调用次数Arg.AnyType()做参数匹配。注意规范原文引用的是接口风格的INavigationService实际仓库中该服务接口为 src/Wpf.Ui/INavigationService.cs编写时可对照真实签名调整。Global Usings规范中 Usings.cs 的位置在仓库中实际对应 tests/Wpf.Ui.UnitTests/GlobalUsings.csglobal using System; global using System.Windows; global using NSubstitute; global using Xunit;借助global using每个测试文件都不再需要重复using NSubstitute;、using Xunit;等语句从而让测试体聚焦于 Arrange/Act/Assert 本身。集成测试项目则在 csproj 中通过Using IncludeAwesomeAssertions /、Using IncludeNSubstitute /、Using IncludeXunit /声明全局导入。集成测试模板与 UiTest 基类命名规范Subject_ShouldExpectedBehavior_WhenCondition即 被测对象_应当产生什么行为_当满足什么条件例如Settings_ShouldBeAvailable_ThroughAutoSuggestBox、CloseButton_ShouldCloseWindow_WhenClicked。基类模式与完整示例所有集成测试继承自UiTest基类实际示例见 tests/Wpf.Ui.Gallery.IntegrationTests/NavigationTests.csusing 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)?.AsAutoSuggestBox(); 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(FuncConditionFactory, 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每个测试都会获得一个独立的应用实例——这得益于TestedApplicationfixturetests/Wpf.Ui.Gallery.IntegrationTests/Fixtures/TestedApplication.csInitializeAsync定位输出目录下的Wpf.Ui.Gallery.exe若不存在直接抛出InvalidOperationException随后通过Application.Launch(path)启动应用并调用WaitWhileMainHandleIsMissing(TimeSpan.FromMinutes(1))等待主窗口句柄出现最多 1 分钟DisposeAsync先Close()关闭应用再用Retry.WhileFalse(...)重试确认进程退出2 秒超时最后释放UIA3Automation。也就是说自动清理状态 由 fixture 兜底测试自身无需管理进程生命周期。Enter与Press的底层都基于 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 IncludeAwesomeAssertions /中整个项目无需显式写 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;verbositydetailed集成测试会真实启动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.cs→TransitionAnimationProviderTests.csSymbolExtensions.cs→SymbolExtensionsTests.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 编写测试的场景给出了纪律性要求编写单元测试时用 NSubstitute 模拟依赖不要构造真实的重型对象只测试公共 API 表面不测私有实现细节使用 XUnit Assert 方法断言严格遵守命名规范每个测试逻辑上只断言一件事成功与失败路径都要覆盖——正如TransitionAnimationProviderTests同时覆盖了负时长与空元素两个失败分支。编写集成测试时继承UiTest基类复用应用生命周期管理使用 AutomationId 定位元素如NavigationAutoSuggestBox、TitleBarCloseButton而非依赖坐标用Wait()给 UI 更新留出时间通常 12 秒使用 AwesomeAssertions 流式语法为断言提供清晰的 because 说明状态清理交给 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 PropertyWPF 控件大量使用依赖属性测试应同时验证默认值与可读写性[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.ForINavigationViewPageProvider(); pageProvider.GetPage(Arg.AnyType()).Returns(new DashboardPage()); var service new NavigationService(pageProvider); var navigationView Substitute.ForINavigationView(); 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可对照真实签名编写。持续集成现状与扩展建议规范明确指出当前测试并未接入 CIPR 校验器只负责构建 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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进