.NETUI测试核心 dotnet-ui-testing-core

此技能提供 .NET UI 测试的核心模式,包括页面对象模型、测试选择器策略、异步等待模式和可访问性测试方法,帮助开发者构建稳定、可维护的 UI 测试套件,适用于 Blazor、MAUI 和 Uno Platform 等多种 .NET UI 框架。关键词:.NET UI 测试、页面对象模型、测试选择器、异步等待、可访问性测试、Blazor 测试、MAUI 测试、Uno Platform 测试、自动化测试、软件测试。

测试 0 次安装 0 次浏览 更新于 3/6/2026

名称:dotnet-ui-testing-core 描述:“跨框架测试 UI。页面对象、测试选择器、异步等待、可访问性。” 用户可调用:false

dotnet-ui-testing-core

适用于 .NET UI 框架(Blazor、MAUI、Uno Platform)的核心 UI 测试模式。涵盖用于可维护测试结构的页面对象模型、用于可靠元素识别的测试选择器策略、用于非确定性 UI 的异步等待模式以及可访问性测试方法。

版本假设: .NET 8.0+ 基线。框架特定细节委托给专用技能。

范围

  • 用于可维护测试结构的页面对象模型
  • 用于可靠元素识别的测试选择器策略
  • 用于非确定性 UI 的异步等待模式
  • 可访问性测试方法

超出范围

  • Blazor 组件测试 (bUnit) – 见 [skill:dotnet-blazor-testing]
  • MAUI UI 测试 (Appium/XHarness) – 见 [skill:dotnet-maui-testing]
  • Uno Platform WASM 测试 – 见 [skill:dotnet-uno-testing]
  • 浏览器自动化详情 – 见 [skill:dotnet-playwright]
  • 测试项目脚手架 – 见 [skill:dotnet-add-testing]

先决条件: 通过 [skill:dotnet-add-testing] 搭建的测试项目。熟悉来自 [skill:dotnet-testing-strategy] 的测试策略决策。

交叉引用:用于决定何时适合 UI 测试的 [skill:dotnet-testing-strategy]、用于基于浏览器的 E2E 自动化的 [skill:dotnet-playwright]、用于 Blazor 组件测试的 [skill:dotnet-blazor-testing]、用于移动/桌面 UI 测试的 [skill:dotnet-maui-testing]、用于 Uno Platform 测试的 [skill:dotnet-uno-testing]。


页面对象模型

页面对象模型(POM)将页面结构和交互封装在类后面,隔离测试与 UI 实现细节。当 UI 更改时,只需更新页面对象——而不是每个接触该页面的测试。

结构

PageObjects/
  LoginPage.cs           -- 登录表单交互
  DashboardPage.cs       -- 仪表板导航 + 小部件
  OrderListPage.cs       -- 订单列表过滤 + 选择
  Components/
    NavigationMenu.cs     -- 共享导航组件
    ConfirmDialog.cs      -- 可重用确认模态框

示例:通用页面对象基类

/// <summary>
/// 页面对象的基类。按框架子类化:
/// Playwright 使用 IPage,bUnit 使用 IRenderedComponent,Appium 使用 AppiumDriver。
/// </summary>
public abstract class PageObjectBase<TDriver>
{
    protected TDriver Driver { get; }

    protected PageObjectBase(TDriver driver)
    {
        Driver = driver;
    }

    /// <summary>
    /// 验证页面/组件在导航后处于预期状态。
    /// 在构造函数或导航后调用此方法以快速失败在错误页面上。
    /// </summary>
    protected abstract void VerifyLoaded();
}

示例:Playwright 页面对象

public class LoginPage : PageObjectBase<IPage>
{
    public LoginPage(IPage page) : base(page)
    {
        VerifyLoaded();
    }

    protected override void VerifyLoaded()
    {
        // 快速失败,如果不在登录页面上
        Driver.WaitForSelectorAsync("[data-testid='login-form']")
            .GetAwaiter().GetResult();
    }

    public async Task<DashboardPage> LoginAsync(string email, string password)
    {
        await Driver.FillAsync("[data-testid='email-input']", email);
        await Driver.FillAsync("[data-testid='password-input']", password);
        await Driver.ClickAsync("[data-testid='login-button']");
        await Driver.WaitForURLAsync("**/dashboard");
        return new DashboardPage(Driver);
    }

    public async Task<string> GetErrorMessageAsync()
    {
        var error = Driver.Locator("[data-testid='login-error']");
        return await error.TextContentAsync() ?? "";
    }
}

// 测试中使用
[Fact]
public async Task Login_ValidCredentials_RedirectsToDashboard()
{
    var loginPage = new LoginPage(Page);

    var dashboard = await loginPage.LoginAsync("user@example.com", "P@ssw0rd!");

    Assert.NotNull(dashboard);
}

页面对象原则

  • 从导航操作返回下一个页面对象。 LoginAsync 返回 DashboardPage,引导测试作者通过应用程序流程。
  • 永远不要从页面对象暴露原始选择器。 测试调用 LoginAsync(),而不是 ClickAsync("#submit")
  • 将断言保留在测试中,而不是页面对象中。 页面对象提供数据(例如,GetErrorMessageAsync());测试对该数据进行断言。
  • 从可重用组件组合页面对象。 NavigationMenu 组件对象可以嵌入到每个有导航栏的页面中。

测试选择器策略

选择器确定测试如何找到 UI 元素。脆弱的选择器是 UI 测试不稳定的主要原因。

选择器优先级(从最可靠到最不可靠)

优先级 选择器类型 示例 可靠性
1 data-testid [data-testid='submit-btn'] 最高——在 CSS/布局更改中存活
2 可访问性角色 + 名称 GetByRole(AriaRole.Button, new() { Name = "Submit" }) 高——与可见行为相关
3 标签文本 GetByLabel("Email address") 高——当副本更改时更改
4 占位符文本 GetByPlaceholder("Enter email") 中等——通常本地化
5 CSS 类 .btn-primary 低——随样式更改
6 XPath / DOM 结构 //div[3]/button[1] 最低——在任何布局更改时中断

添加测试 ID

向测试交互的元素添加 data-testid 属性。它们对用户不可见且在重构中稳定:

Blazor:

<button data-testid="submit-order" @onclick="SubmitOrder">下订单</button>
<input data-testid="search-input" @bind="SearchTerm" />

MAUI XAML:

<Button AutomationId="submit-order" Text="下订单" Clicked="OnSubmit" />
<Entry AutomationId="search-input" Text="{Binding SearchTerm}" />

Uno Platform XAML:

<Button AutomationProperties.AutomationId="submit-order" Content="下订单" />

选择器反模式

// 错误:绑定到 CSS 实现
await page.ClickAsync(".MuiButton-root.MuiButton-containedPrimary");

// 错误:绑定到 DOM 结构
await page.ClickAsync("div > form > div:nth-child(3) > button");

// 错误:绑定到动态内容
await page.ClickAsync($"text=Order #{orderId}");

// 好:稳定的测试标识符
await page.ClickAsync("[data-testid='submit-order']");

// 好:可访问性驱动(Playwright)
await page.GetByRole(AriaRole.Button, new() { Name = "下订单" }).ClickAsync();

异步等待策略

UI 测试处理异步渲染、网络请求和动画。硬编码延迟会导致不稳定测试和慢速测试套件。

等待策略决策树

元素是否已在 DOM 中?
|
+-- 是 --> 它是否可见且可操作?
|           |
|           +-- 是 --> 立即交互
|           +-- 否 --> 等待可见/启用状态
|
+-- 否 --> 等待元素出现在 DOM 中
            |
            是否通过网络请求加载?
            |
            +-- 是 --> 等待网络空闲或特定 API 响应
            +-- 否 --> 等待渲染周期完成

框架特定等待模式

Playwright(基于浏览器):

// 自动等待:Playwright 默认等待可操作性
await page.ClickAsync("[data-testid='submit']"); // 等待直到可见 + 启用

// 显式等待网络加载内容
await page.WaitForResponseAsync(
    response => response.Url.Contains("/api/orders") && response.Status == 200);

// 等待元素状态
await page.Locator("[data-testid='results']")
    .WaitForAsync(new() { State = WaitForSelectorState.Visible });

// 等待特定文本内容
await Expect(page.Locator("[data-testid='status']")).ToHaveTextAsync("已完成");

bUnit(Blazor 组件测试):

// 等待异步状态更改以渲染
var cut = RenderComponent<OrderList>();

// 等待组件完成异步操作
cut.WaitForState(() => cut.Instance.Orders.Count > 0,
    timeout: TimeSpan.FromSeconds(5));

// 等待特定标记
cut.WaitForAssertion(() =>
    Assert.NotEmpty(cut.FindAll("[data-testid='order-row']")),
    timeout: TimeSpan.FromSeconds(5));

等待反模式

// 错误:硬编码延迟——慢且仍然不稳定
await Task.Delay(3000);
await page.ClickAsync("[data-testid='results']");

// 错误:使用 Thread.Sleep 轮询
while (!element.IsVisible)
{
    Thread.Sleep(100); // 阻塞线程,无超时安全性
}

// 好:框架原生等待
await page.Locator("[data-testid='results']")
    .WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = 5000 });

// 好:带重试的断言(Playwright)
await Expect(page.Locator("[data-testid='count']")).ToHaveTextAsync("5");

可访问性测试

可访问性测试验证 UI 组件是否对残疾人可用并与辅助技术兼容。自动检查捕捉常见问题;主观标准仍需手动审查。

使用 Playwright 的自动可访问性检查

// NuGet:Deque.AxeCore.Playwright
[Fact]
public async Task HomePage_PassesAccessibilityAudit()
{
    await Page.GotoAsync("/");

    var results = await Page.RunAxe();

    Assert.Empty(results.Violations);
}

[Fact]
public async Task OrderForm_NoAccessibilityViolations()
{
    await Page.GotoAsync("/orders/new");

    // 范围到特定组件
    var form = Page.Locator("[data-testid='order-form']");
    var results = await Page.RunAxe(new AxeRunOptions
    {
        // 专注于 WCAG 2.1 AA 规则
        RunOnly = new RunOnlyOptions
        {
            Type = "tag",
            Values = ["wcag2a", "wcag2aa", "wcag21aa"]
        }
    });

    // 报告违规细节以调试
    foreach (var violation in results.Violations)
    {
        // 记录:violation.Id、violation.Description、violation.Nodes
    }
    Assert.Empty(results.Violations);
}

UI 测试的可访问性检查清单

检查 如何测试 工具
颜色对比度 自动 axe-core 规则 Deque.AxeCore.Playwright
键盘导航 通过所有交互元素标签 Playwright page.Keyboard
ARIA 标签 验证 aria-label / aria-labelledby 存在 Playwright 定位器 + 断言
焦点管理 验证焦点移动到对话框/模态框 Playwright page.Locator(':focus')
屏幕阅读器文本 验证 aria-live 区域更新 手动 + 对 ARIA 属性的断言

键盘导航测试示例

[Fact]
public async Task OrderForm_TabOrder_FollowsLogicalSequence()
{
    await Page.GotoAsync("/orders/new");

    // 通过表单字段标签并验证焦点顺序
    await Page.Keyboard.PressAsync("Tab");
    await Expect(Page.Locator("[data-testid='customer-name']")).ToBeFocusedAsync();

    await Page.Keyboard.PressAsync("Tab");
    await Expect(Page.Locator("[data-testid='customer-email']")).ToBeFocusedAsync();

    await Page.Keyboard.PressAsync("Tab");
    await Expect(Page.Locator("[data-testid='order-items']")).ToBeFocusedAsync();

    // 验证 Enter 提交表单
    await Page.Keyboard.PressAsync("Tab"); // 焦点提交按钮
    await Expect(Page.Locator("[data-testid='submit-order']")).ToBeFocusedAsync();
}

关键原则

  • 对任何超过少量测试的 UI 测试套件使用页面对象模型。 前期成本很快在减少维护中回报。
  • 优先使用 data-testid 或基于可访问性的选择器而非 CSS 或 DOM 结构选择器。 稳定的选择器是对不稳定测试的最有效防御。
  • 永远不要使用 Thread.SleepTask.Delay 作为等待策略。 使用框架原生等待,在超时下轮询条件。
  • 将可访问性检查作为标准测试套件的一部分运行, 而不是作为单独审计。 早期捕捉违规防止可访问性债务。
  • 在可能的情况下保持页面对象框架无关。 模式(POM、选择器策略、等待模式)是通用的;仅驱动程序 API 在 Playwright、bUnit 和 Appium 之间更改。

代理陷阱

  1. 未经团队同意,不要在生产代码中添加 data-testid 属性。 有些团队在生产构建中移除它们;另一些保留它们。首先检查项目的约定。
  2. 不要在 Playwright 测试中使用 WaitForTimeout(硬编码延迟)。 它掩盖时机问题并使测试变慢。改用 WaitForSelectorAsyncExpect(...).ToBeVisibleAsync()WaitForResponseAsync
  3. 不要断言元素数量而不等待列表加载。 FindAll("[data-testid='row']").Count 如果组件未完成渲染,返回零。首先使用 WaitForStateWaitForAssertion
  4. 不要因为“不是要求”而跳过可访问性测试。 WCAG 合规性日益成为法律要求。自动检查以几乎零成本捕捉低挂果实。
  5. 不要创建深度嵌套的页面对象。 如果页面对象中有页面对象,请展平层次结构。一级组件组合(页面 -> 组件)足够。

参考资料