WPF应用测试实战:从单元测试到UI自动化的完整策略 1. 项目概述为什么WPF测试是开发者的必修课在桌面应用开发领域WPF以其强大的数据绑定、灵活的样式模板和声明式的XAML界面一直是构建复杂、美观客户端应用的首选框架之一。然而随着项目规模的增长一个老生常谈的问题会逐渐浮出水面如何保证代码质量尤其是在UI逻辑日益复杂之后很多开发者包括我自己在早期都曾陷入“写代码一时爽维护火葬场”的窘境。一个看似简单的按钮点击事件背后可能串联着数据验证、业务逻辑调用、界面状态更新和异步操作手动测试每一个分支路径几乎是不可能的任务。这正是“单元测试”和“UI测试”的价值所在——它们不是给项目增加负担的“额外工作”而是保障开发效率、提升代码信心的“基础设施”。这个项目就是一次关于WPF应用测试的深度实战。它不仅仅是一份操作手册更是一套从思想到实践的完整策略。我们将从最基础的单元测试框架搭建开始逐步深入到如何为MVVM模式下的ViewModel编写纯净的测试最后攻克UI自动化测试这个公认的难点。你会发现高效的测试并非遥不可及通过合理的工具选型和设计模式完全可以将测试融入日常开发流程让它成为你交付可靠软件的得力助手。无论你是正在为遗留的WPF项目引入测试而头疼还是准备启动一个全新的项目并希望从一开始就构建健壮的架构这里的内容都将为你提供清晰的路径和可复现的代码示例。2. 测试策略总览单元测试与UI测试的分工与协作在动手写第一行测试代码之前我们必须理清单元测试和UI测试各自的职责边界以及它们如何协同工作。混淆两者的概念是导致测试代码难以维护、运行缓慢的常见根源。单元测试顾名思义测试的是软件中最小可测试单元在C#中通常是一个方法或一个类的行为是否符合预期。在WPF的上下文中尤其是在采用MVVM模式时我们的核心单元就是ViewModel。单元测试的核心特点是快速、隔离、可重复。它不应该启动整个应用程序不应该依赖数据库、网络服务或文件系统这些依赖需要通过Mock或Stub来模拟更不应该与真实的UI控件交互。一个理想的ViewModel单元测试可以在几毫秒内运行完毕并且结果百分之百确定。UI测试或称为自动化UI测试、端到端测试则站在用户的角度模拟真实用户的操作流程如点击按钮、输入文本、验证界面元素的显示状态等。它测试的是整个功能链条的集成效果包括UI交互、业务逻辑和数据流的正确性。UI测试运行速度慢稳定性相对较低容易受界面布局变化、异步加载等因素影响但它是验证核心用户旅程是否畅通无阻的最后一道防线。它们的关系可以这样理解单元测试是“地基”和“承重墙”确保每一块砖业务逻辑都坚固可靠UI测试是“装修验收”确保整个房子用户界面用起来舒服、功能齐全。我们应该用大量的单元测试覆盖所有复杂的业务逻辑和状态转换而只用少量的、关键的UI测试来覆盖最重要的用户操作路径。2.1 核心工具链选型为什么是它们工欲善其事必先利其器。经过多年的社区实践和项目锤炼一套稳定高效的WPF测试工具链已经非常成熟。1. 单元测试框架xUnit在.NET生态中NUnit和MSTest曾是主流但xUnit凭借其更简洁的设计哲学例如一个测试类对应一个测试实例避免了测试间的意外状态共享和活跃的社区已成为许多新项目的首选。它的断言方式Assert.EqualAssert.True直观并且与.NET CLI工具链集成得非常好。2. 模拟框架Moq这是.NET领域事实上的标准模拟库。它的API设计流畅可以非常方便地创建接口或虚方法的模拟对象Mock并设置这些模拟对象的行为返回指定值、抛出异常等和验证对其的调用。在测试ViewModel时我们用它来模拟IDataService、IMessageDialog等外部依赖。3. UI测试框架Appium WinAppDriver对于WPF桌面应用的UI自动化WinAppDriverWindows Application Driver是一个微软官方支持的开源项目它实现了WebDriver协议使得我们可以用编写Web自动化测试类似的方式来编写Windows桌面应用测试。Appium则是一个跨平台的移动/桌面应用自动化框架它内置了对WinAppDriver的支持提供了更上层的、语言无关的API。选择这套组合意味着你可以使用熟悉的Selenium WebDriver模式如通过FindElementByAccessibilityId定位元素来编写测试并且测试脚本可以用C#、Python、Java等多种语言编写。4. 测试运行与报告Visual Studio Test Explorer / dotnet test这是我们的主战场。无论是单元测试还是UI测试启动应用后的集成测试都可以通过Visual Studio的测试资源管理器来方便地运行、调试和查看结果。对于CI/CD流水线则使用dotnet test命令行工具来执行测试并生成结果报告如TRX格式或用于Azure DevOps的VSTest格式。注意市面上也有一些其他UI测试方案如基于UI Automation的White框架或TestComplete等商业工具。选择AppiumWinAppDriver的主要原因在于其开源、标准化WebDriver协议和强大的社区支持能与现有的Web测试技能栈无缝衔接长期维护成本相对较低。3. 构建可测试的WPF应用架构MVVM是基石在深入测试代码之前我们必须确保被测试的应用程序本身是“可测试的”。一个高度耦合、UI逻辑与业务逻辑混杂的WinForms风格代码几乎无法进行有效的单元测试。因此采用MVVMModel-View-ViewModel模式不是可选项而是实现高效测试的必选项。MVVM的核心思想是关注点分离Model代表数据和业务规则是独立于UI的领域模型。View就是XAML文件负责定义UI的结构、样式和动画它应该尽可能“笨”只包含与呈现相关的逻辑。ViewModel是View的抽象它包含View所需的数据属性和命令操作。它通过数据绑定与View连接并调用Model或服务来完成业务逻辑。这种分离带来的最大好处就是ViewModel不依赖任何具体的UI控件。它只是一个普通的C#类暴露了一系列属性和命令。这意味着我们可以像测试任何一个普通类一样在单元测试中实例化ViewModel设置其属性调用其命令然后验证其状态变化整个过程完全不需要启动WPF应用程序或加载XAML。3.1 实现一个可测试的ViewModel示例让我们从一个简单的登录场景开始。假设我们有一个LoginViewModel它需要用户名、密码提供一个登录命令并调用一个认证服务。首先定义依赖的服务接口这是实现依赖注入和模拟的关键public interface IAuthenticationService { Taskbool AuthenticateAsync(string username, string password); } public interface INavigationService { void NavigateToMain(); void ShowError(string message); }接着实现LoginViewModel。注意所有依赖都通过构造函数注入public class LoginViewModel : INotifyPropertyChanged { private readonly IAuthenticationService _authService; private readonly INavigationService _navigationService; private string _username; private string _password; private bool _isBusy; public event PropertyChangedEventHandler PropertyChanged; public string Username { get _username; set { _username value; OnPropertyChanged(); LoginCommand.RaiseCanExecuteChanged(); } } public string Password { get _password; set { _password value; OnPropertyChanged(); LoginCommand.RaiseCanExecuteChanged(); } } public bool IsBusy { get _isBusy; private set { _isBusy value; OnPropertyChanged(); LoginCommand.RaiseCanExecuteChanged(); } } public ICommand LoginCommand { get; } public LoginViewModel(IAuthenticationService authService, INavigationService navigationService) { _authService authService ?? throw new ArgumentNullException(nameof(authService)); _navigationService navigationService ?? throw new ArgumentNullException(nameof(navigationService)); LoginCommand new RelayCommand(async () await ExecuteLoginAsync(), CanExecuteLogin); } private bool CanExecuteLogin() !IsBusy !string.IsNullOrWhiteSpace(Username) !string.IsNullOrWhiteSpace(Password); private async Task ExecuteLoginAsync() { if (IsBusy) return; IsBusy true; try { var isAuthenticated await _authService.AuthenticateAsync(Username, Password); if (isAuthenticated) { _navigationService.NavigateToMain(); } else { _navigationService.ShowError(用户名或密码错误。); } } catch (Exception ex) { _navigationService.ShowError($登录过程中发生错误{ex.Message}); } finally { IsBusy false; } } protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }实操心得在ViewModel中对于命令CanExecute的判断逻辑我强烈建议像上面一样将其抽离成一个独立的CanExecuteLogin方法并在相关属性变化时调用RaiseCanExecuteChanged。这比在命令的CanExecute委托内写复杂的逻辑更清晰也更容易被单元测试覆盖。同时注意IsBusy属性的使用它在异步操作期间禁用UI这是防止用户重复提交、提升体验的通用模式也方便测试验证状态。4. 为ViewModel编写高效的单元测试现在我们有了一个设计良好的LoginViewModel为它编写单元测试将变得非常直接。我们将使用xUnit和Moq。首先在测试项目中安装必要的NuGet包xunit,xunit.runner.visualstudio,Moq。4.1 测试用例设计与编写一个健壮的测试应该覆盖正常路径Happy Path和多种异常路径。对于LoginViewModel我们可以设计以下测试登录成功测试验证当认证服务返回true时会调用导航服务跳转到主页面。登录失败测试验证当认证服务返回false时会调用导航服务显示错误信息。网络或服务异常测试验证当认证服务抛出异常时会捕获异常并显示错误信息。命令可执行性测试验证LoginCommand的CanExecute逻辑是否正确例如当用户名或密码为空时不可用当IsBusy为true时不可用。下面是第一个测试“登录成功”的示例using Xunit; using Moq; public class LoginViewModelTests { [Fact] public async Task ExecuteLoginAsync_WithValidCredentials_CallsNavigateToMain() { // 1. 准备阶段 (Arrange) var mockAuthService new MockIAuthenticationService(); var mockNavService new MockINavigationService(); // 设置模拟认证服务使其在接收到特定用户名密码时返回 true mockAuthService .Setup(service service.AuthenticateAsync(testUser, testPass)) .ReturnsAsync(true); // 模拟成功认证 var viewModel new LoginViewModel(mockAuthService.Object, mockNavService.Object) { Username testUser, Password testPass }; // 2. 执行阶段 (Act) // 直接调用命令的执行委托。注意我们测试的是ViewModel内部的逻辑不直接测试ICommand接口。 // 更准确的方式是触发Command但为简化示例我们调用其背后的异步方法。 await viewModel.ExecuteLoginAsync(); // 3. 断言阶段 (Assert) // 验证导航服务的 NavigateToMain 方法被调用了一次且仅一次 mockNavService.Verify(service service.NavigateToMain(), Times.Once); // 验证 ShowError 方法从未被调用 mockNavService.Verify(service service.ShowError(It.IsAnystring()), Times.Never); // 验证认证服务被以正确的参数调用了一次 mockAuthService.Verify(service service.AuthenticateAsync(testUser, testPass), Times.Once); // 验证 IsBusy 状态在操作后已复位为 false Assert.False(viewModel.IsBusy); } }4.2 模拟Mock与存根Stub的深度使用技巧Moq的核心是MockT类。Setup方法用于“安排”模拟对象的行为Verify方法用于“断言”模拟对象的交互是否符合预期。严格模拟 vs 宽松模拟默认情况下Moq创建的是“宽松”模拟。这意味着如果你没有为某个方法Setup调用它时会返回默认值如null、0、false或空任务而不会抛出异常。这在测试中通常是有益的因为你只关心你明确设置和验证的交互。如果你希望任何未预期的调用都导致测试失败可以在创建Mock时使用MockBehavior.Strict但这会让测试变得更脆弱。参数匹配器It.IsAnystring()表示匹配任何字符串参数。It.Isstring(s s.StartsWith(“admin”))可以匹配更复杂的条件。这在验证方法调用或设置返回值时非常有用。验证调用顺序Moq本身不直接支持验证多个方法调用的顺序。如果调用顺序对你的业务逻辑至关重要你可能需要记录调用时间戳或考虑使用更高级的模拟框架或者重新审视设计是否过于复杂。模拟异步方法对于返回Task或TaskT的方法使用.ReturnsAsync(T value)来设置返回值。如果需要模拟一个异步方法抛出异常使用.ThrowsAsync(new Exception(“…”))。4.3 测试异步命令与属性通知测试异步命令时要特别注意死锁问题。在单元测试中我们通常不希望引入真实的同步上下文SynchronizationContext。上面的测试方法被标记为async Task并且我们直接await了ViewModel的异步方法这在大多数情况下是安全的因为我们的测试运行在默认的线程池上下文上。对于实现了INotifyPropertyChanged的ViewModel测试属性变更通知是另一项关键任务。虽然xUnit没有内置对此的支持但我们可以通过订阅PropertyChanged事件来手动验证[Fact] public void SettingUsername_RaisesPropertyChanged() { var viewModel new LoginViewModel(Mock.OfIAuthenticationService(), Mock.OfINavigationService()); string changedPropertyName null; viewModel.PropertyChanged (sender, args) changedPropertyName args.PropertyName; viewModel.Username NewUser; Assert.Equal(nameof(LoginViewModel.Username), changedPropertyName); }踩坑记录早期我经常忘记在属性设置器中调用OnPropertyChanged或者拼错属性名。编写这类测试能有效捕获这类低级错误。同时测试CanExecute逻辑时要确保在相关属性变化后命令的可执行状态得到了更新这可以通过检查LoginCommand.CanExecute(null)的结果来验证。5. 搭建WPF UI自动化测试环境单元测试保障了后台逻辑的坚固但用户最终接触的是UI。UI自动化测试能让我们以编程方式模拟用户操作验证界面反馈。我们选择Appium WinAppDriver的方案。5.1 环境配置详解启用Windows开发者模式在Windows设置中找到“开发者设置”开启“开发人员模式”。这是安装WinAppDriver所必需的。安装WinAppDriver从GitHub Releases页面下载最新的WindowsApplicationDriver.msi安装包。运行安装程序。安装完成后默认会在C:\Program Files (x86)\Windows Application Driver下。你可以通过命令行运行WinAppDriver.exe来启动服务默认监听http://127.0.0.1:4723。为了自动化更推荐将其安装为Windows服务# 以管理员身份打开CMD进入安装目录 cd “C:\Program Files (x86)\Windows Application Driver” WinAppDriver.exe /silent这会在后台启动服务。你可以在任务管理器的“服务”选项卡中找到WAD服务进行启动/停止。配置测试项目在你的UI测试项目中可以是一个独立的xUnit或NUnit项目通过NuGet安装Appium.WebDriver。同时你需要确保你的WPF应用程序编译后的.exe路径是已知的以便测试启动它。5.2 编写第一个UI测试登录场景自动化假设我们有一个简单的WPF登录窗口其中包含两个TextBox分别有AutomationProperties.AutomationId”UsernameBox”和”PasswordBox”和一个ButtonAutomationId”LoginButton”。为UI元素设置AutomationId是UI自动化测试成功的关键它提供了稳定、唯一的定位方式。using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; using Xunit; public class LoginWindowUITests : IDisposable { private const string WindowsApplicationDriverUrl “http://127.0.0.1:4723”; private const string AppPath ”C:\Path\To\Your\WpfApp.exe”; private WindowsDriverWindowsElement _driver; public LoginWindowUITests() { if (_driver null) { var appiumOptions new AppiumOptions(); appiumOptions.AddAdditionalCapability(“app”, AppPath); appiumOptions.AddAdditionalCapability(“platformName”, “Windows”); appiumOptions.AddAdditionalCapability(“deviceName”, “WindowsPC”); // 注意对于已启动的应用可以使用 “appTopLevelWindow” 能力通过窗口句柄来附加 // appiumOptions.AddAdditionalCapability(“appTopLevelWindow”, “0x123456”); _driver new WindowsDriverWindowsElement(new Uri(WindowsApplicationDriverUrl), appiumOptions); // 设置隐式等待让查找元素操作在超时前等待一段时间 _driver.Manage().Timeouts().ImplicitWait TimeSpan.FromSeconds(5); } } [Fact] public void LoginWithValidCredentials_ShouldNavigateToMainWindow() { // 1. 定位元素 var usernameBox _driver.FindElementByAccessibilityId(“UsernameBox”); var passwordBox _driver.FindElementByAccessibilityId(“PasswordBox”); var loginButton _driver.FindElementByAccessibilityId(“LoginButton”); // 2. 执行操作 usernameBox.SendKeys(“testuser”); passwordBox.SendKeys(“password123”); loginButton.Click(); // 3. 等待并验证结果 // 假设登录成功后当前窗口的标题会改变或者会出现新的窗口 // 这里我们使用显式等待来等待新窗口或元素出现 var wait new WebDriverWait(_driver, TimeSpan.FromSeconds(10)); // 例如等待主窗口的某个特征元素出现 bool isMainWindowLoaded wait.Until(drv { try { // 尝试查找主窗口特有的元素例如一个 AutomationId 为 “MainHeader” 的文本 var mainHeader drv.FindElementByAccessibilityId(“MainHeader”); return mainHeader.Displayed; } catch (NoSuchElementException) { return false; } }); Assert.True(isMainWindowLoaded, “登录后未能成功跳转到主窗口。”); } public void Dispose() { if (_driver ! null) { // 关闭应用 _driver.CloseApp(); _driver.Quit(); _driver null; } } }5.3 元素定位策略与等待机制UI测试的稳定性很大程度上取决于元素定位和时机处理。首选AccessibilityId这对应WPF中的AutomationProperties.AutomationId属性。它是为自动化测试设计的唯一标识符是最稳定、最推荐的定位方式。其他定位器如果无法设置AutomationId可以退而求其次使用Name通常是控件的Content或Text、ClassName如”Button”或XPath。但XPath在桌面应用中可能性能较差且易受UI结构变化影响。显式等待是必须的永远不要使用Thread.Sleep进行固定时间的等待。应使用WebDriverWait配合ExpectedConditions在Selenium中或自定义条件函数如上例。这能确保测试在元素就绪后立即继续既提高了速度又增加了稳定性。处理多窗口WPF应用可能弹出对话框或打开新窗口。使用_driver.SwitchTo().Window(windowHandle)可以在窗口间切换。在测试开始时可以通过_driver.WindowHandles获取所有窗口句柄。避坑指南UI测试最大的敌人是“脆性”。界面微小的改动如一个按钮的AutomationId变了就可能导致测试失败。因此为关键的可交互控件设置稳定、有意义的AutomationId并将其视为与API接口一样重要的契约。同时将页面元素的定位信息如AccessibilityId集中管理在一个“页面对象模型”类中可以减少重复代码并在UI变更时只需修改一处。6. 集成测试连接单元测试与UI测试的桥梁纯粹的单元测试不涉及UI而UI测试又太重。有时我们需要一种中间层次的测试它启动应用程序或其主要模块但以编程方式驱动并直接与ViewModel或服务层交互绕过真实的UI控件。这种测试通常被称为集成测试或功能测试。对于WPF MVVM应用一个非常有效的集成测试策略是在测试中启动应用程序获取根容器如MainWindow然后通过数据上下文DataContext直接访问并操作其ViewModel。6.1 实现一个View-ViewModel集成测试假设我们的应用使用依赖注入容器如Prism的IContainerExtension或Microsoft.Extensions.DependencyInjection来组合视图和ViewModel。在测试中我们可以复用或模拟这个容器来创建View和ViewModel。[Fact] public void MainWindowLoads_DataContextIsCorrectViewModel() { // 此测试需要引用你的WPF主项目 var app new App(); // 启动WPF应用程序实例 app.InitializeComponent(); app.Startup (s, e) { // 在UI线程上获取主窗口 var mainWindow Application.Current.MainWindow as MainWindow; Assert.NotNull(mainWindow); // 验证DataContext的类型 var viewModel mainWindow.DataContext as MainViewModel; Assert.NotNull(viewModel); // 可以直接操作ViewModel viewModel.LoadDataCommand.Execute(null); // 可以断言ViewModel的状态例如数据是否加载 Assert.NotEmpty(viewModel.Items); }; // 注意这种测试方式需要小心处理应用程序的生命周期和线程关联性。 // 更常见的做法是使用UI测试框架来附加到已运行的应用或者使用特殊的测试启动器。 }实际上更可靠的做法是编写一种“无头”的集成测试。例如使用Microsoft.AspNetCore.Mvc.Testing测试Web API那样我们也可以为WPF应用创建一个TestHost。但这需要更多的基础设施支持例如将应用的核心逻辑抽象到一个可独立于UI运行的“应用核心”库中。对于大多数项目通过UI测试框架Appium来模拟用户操作已经足够覆盖从UI到业务逻辑的集成路径。6.2 测试数据管理与测试隔离无论是单元测试还是集成测试测试隔离都至关重要。每个测试都应该在一个干净、已知的状态下开始和结束。使用内存数据库如果应用使用数据库在集成测试中使用像SQLite内存数据库或Entity Framework Core的In-Memory Provider来替代真实数据库。确保在每个测试开始时迁移数据库并填充种子数据测试结束后清理。模拟外部服务像单元测试一样使用Moq来模拟HTTP API客户端、文件系统操作等外部依赖。清理资源在测试类的Dispose方法或IAsyncLifetime接口中确保关闭应用、删除临时文件、关闭数据库连接等。7. 将测试融入开发流程CI/CD与代码覆盖率测试的价值在于持续运行。将自动化测试集成到持续集成/持续部署流水线中是保证代码质量不断反馈的关键。在CI中运行测试在Azure DevOps Pipelines、GitHub Actions或Jenkins中添加一个构建步骤来运行dotnet test命令。你可以为单元测试和UI测试分别创建不同的任务或阶段。由于UI测试需要图形界面你需要在自托管代理或配置了图形环境的托管代理如Azure DevOps的windows-latest镜像上运行它们。# GitHub Actions 示例片段 - name: Run Unit Tests run: dotnet test --configuration Release --logger “trx;LogFileNameunit-tests.trx” --no-build - name: Run UI Tests run: dotnet test --configuration Release --logger “trx;LogFileNameui-tests.trx” --no-build env: # 可能需要设置一些环境变量来指向你的应用路径 APP_PATH: ${{ github.workspace }}/src/MyWpfApp/bin/Release/net8.0-windows/MyWpfApp.exe收集代码覆盖率了解你的测试覆盖了多大比例的代码能帮助你发现测试盲区。使用像coverlet.collector这样的工具来收集覆盖率数据并生成报告。dotnet test --collect:”XPlat Code Coverage”这会在TestResults目录下生成.coverage文件。你可以使用ReportGenerator工具将其转换为HTML等易读格式。测试报告与通知将测试结果TRX文件发布到CI系统使其可视化。如果测试失败CI系统应能自动通知相关开发者如通过邮件、Slack等。个人体会不要追求100%的代码覆盖率那通常成本极高且意义有限。应关注核心业务逻辑、复杂算法和容易出错的边界条件的覆盖率。将UI测试作为“冒烟测试”在每次合并到主分支前运行确保核心功能不被破坏。而全面的单元测试则可以在每次代码提交时快速运行提供即时反馈。8. 常见问题排查与调试技巧实录在实际操作中你一定会遇到各种问题。这里记录了一些典型问题的解决思路。单元测试常见问题测试通过但实际功能有问题检查Mock的设置是否过于宽松。你可能模拟了一个总是成功的方法但实际代码中该方法可能失败。尝试使用MockBehavior.Strict或在测试中验证更具体的交互。异步测试死锁如果在测试中调用了需要特定同步上下文如Dispatcher的代码可能会死锁。确保在单元测试中避免使用.Result或.Wait()始终使用async/await。对于必须使用Dispatcher的代码可以在测试中提供一个使用Task.Run或TaskCompletionSource的模拟实现。“对象引用未设置为对象的实例”检查ViewModel的构造函数注入确保所有依赖项在测试中都被正确模拟Mock并提供。UI测试常见问题WinAppDriver无法启动或连接失败检查Windows Application Driver服务是否正在运行服务名WAD。检查防火墙是否阻止了端口4723。尝试以管理员身份运行WinAppDriver.exe。找不到元素NoSuchElementException首要检查AutomationId是否设置正确在UI运行时可以使用Inspect.exeWindows SDK工具或FlaUInspect等工具查看元素的自动化属性。元素是否尚未加载增加隐式或显式等待时间。元素是否在另一个窗口或框架内需要先切换上下文。对于动态内容如列表元素可能不在当前视图中。尝试使用滚动操作或使用FindElements复数来查找。元素交互失败如Click不生效元素可能被禁用Enabled属性为false或被其他元素遮挡。尝试使用其他交互方式如SendKeys(Keys.Enter)代替点击按钮。对于复杂的自定义控件标准的交互方法可能无效可能需要使用Actions类执行低级鼠标/键盘操作或者通过UI Automation模式如InvokePattern来触发。测试不稳定有时过有时不过这是UI自动化测试的常态。强化等待策略是唯一出路。使用显式等待等待特定的条件成立如元素可点击、元素包含特定文本。在关键操作前后添加短暂的Thread.Sleep作为最后手段但需注明原因。考虑使用重试机制。对于非确定性的失败可以在测试逻辑中包装一个重试循环。调试UI测试当UI测试失败时不要只盯着错误日志看。可以在测试中插入截图功能_driver.GetScreenshot().SaveAsFile(“error.png”)在失败时保存当前界面状态。在运行测试时不要将测试窗口最小化观察应用的自动操作过程往往能直观地发现问题所在。在测试代码中设置断点并使用Visual Studio的调试器附加到测试进程进行调试。将测试融入WPF开发初期确实需要投入时间搭建框架和编写用例但这份投资回报巨大。它带来的不仅是更少的bug和更快的发布周期更重要的是一种“安全网”带来的开发信心——你可以大胆地重构代码因为你知道有测试会告诉你是否破坏了现有功能。从今天开始为你下一个ViewModel方法或UI功能编写一个测试吧你会发现编写可测试的代码本身就是一种更好的软件设计实践。