mcpField
D
dotnet-ui-testing-core

dotnet-ui-testing-core

github.com/wshaddix测试与质量8684 次安装35 周前更新
代码仓库

关于 dotnet-ui-testing-core

面向 .NET UI 测试开发者的跨框架测试模式指南,适用于 Blazor、MAUI 和 Uno Platform。内容聚焦可维护的页面对象、稳定的元素选择器、异步等待和无障碍检查。

主要功能

  1. 01页面对象模式
  2. 02可靠的元素选择策略
  3. 03基于条件的异步等待
  4. 04自动化无障碍检查
  5. 05适用于多种 .NET UI 框架

适用场景

  1. 01组织可维护的 UI 测试套件
  2. 02减少因选择器不稳定导致的测试波动
  3. 03等待异步渲染和网络响应
  4. 04在测试中检查键盘导航与 ARIA

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-ui-testing-core

安装后在 Claude Code 中输入/dotnet-ui-testing-core确认已加载。

详细介绍

dotnet-ui-testing-core

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

**版本假设:**基线为 .NET 8.0+。特定框架的详细内容由专门的技能提供。

**范围之外:**特定框架的测试 API 由相应技能负责:[skill:dotnet-blazor-testing] 负责 bUnit,[skill:dotnet-maui-testing] 负责 Appium/XHarness,[skill:dotnet-uno-testing] 负责 Uno WASM 测试。浏览器自动化的具体内容由 [skill:dotnet-playwright] 负责。测试项目脚手架由 [skill:dotnet-add-testing] 负责。

**前置条件:**已通过 [skill:dotnet-add-testing] 搭建测试项目。熟悉 [skill:dotnet-testing-strategy] 中的测试策略决策。

交叉参考:[skill:dotnet-testing-strategy] 用于判断何时适合使用 UI 测试,[skill:dotnet-playwright] 用于基于浏览器的 E2E 自动化,[skill:dotnet-blazor-testing] 用于 Blazor 组件测试,[skill:dotnet-maui-testing] 用于移动端/桌面端 UI 测试,[skill:dotnet-uno-testing] 用于 Uno Platform 测试。


页面对象模型

页面对象模型(POM)通过类封装页面结构和交互,将测试与 UI 实现细节隔离。当 UI 发生变化时,只需更新页面对象,而不必修改所有涉及该页面的测试。

结构
PageObjects/
  LoginPage.cs           -- login form interactions
  DashboardPage.cs       -- dashboard navigation + widgets
  OrderListPage.cs       -- order list filtering + selection
  Components/
    NavigationMenu.cs     -- shared nav component
    ConfirmDialog.cs      -- reusable confirmation modal
示例:通用页面对象基类
/// <summary>
/// Base class for page objects. Subclass per framework:
/// Playwright uses IPage, bUnit uses IRenderedComponent, Appium uses AppiumDriver.
/// </summary>
public abstract class PageObjectBase<TDriver>
{
    protected TDriver Driver { get; }

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

    /// <summary>
    /// Verifies the page/component is in the expected state after navigation.
    /// Call this in the constructor or after navigation to fail fast on wrong pages.
    /// </summary>
    protected abstract void VerifyLoaded();
}
示例:Playwright 页面对象
public class LoginPage : PageObjectBase<IPage>
{
    public LoginPage(IPage page) : base(page)
    {
        VerifyLoaded();
    }

    protected override void VerifyLoaded()
    {
        // Fail fast if not on the login page
        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() ?? "";
    }
}

// Usage in test
[Fact]
public async Task Login_ValidCredentials_RedirectsToDashboard()
{
    var loginPage = new LoginPage(Page);

    var dashboard = await loginPage.LoginAsync("[email protected]", "P@ssw0rd!");

    Assert.NotNull(dashboard);
}
页面对象原则
  • 导航操作应返回下一个页面对象。 LoginAsync 返回 DashboardPage,引导测试编写者依照应用流程进行操作。
  • 切勿从页面对象中公开原始选择器。 测试应调用 LoginAsync(),而不是 ClickAsync("#submit")。
  • 断言应放在测试中,而非页面对象中。 页面对象提供数据(例如,GetErrorMessageAsync());测试根据这些数据进行断言。
  • 通过可复用组件组合页面对象。 每个包含导航栏的页面都可以嵌入 NavigationMenu 组件对象。

测试选择器策略

选择器决定测试如何查找 UI 元素。脆弱的选择器是导致 UI 测试不稳定的首要原因。

选择器优先级(从高到低)
优先级选择器类型示例可靠性
1data-testid[data-testid='submit-btn']最高——可经受 CSS/布局变更
2无障碍角色 + 名称GetByRole(AriaRole.Button, new() { Name = "Submit" })高——与可见行为相关
3标签文本GetByLabel("Email address")高——文案变更时也会变化
4占位文本GetByPlaceholder("Enter email")中——通常会本地化
5CSS 类.btn-primary低——样式变更时会变化
6XPath / DOM 结构//div[3]/button[1]最低——任何布局变更都可能导致失效
添加测试 ID

为测试交互的元素添加 data-testid 属性。这些属性对用户不可见,且在重构后保持稳定:

Blazor:

<button data-testid="submit-order" @onclick="SubmitOrder">Place Order</button>
<input data-testid="search-input" @bind="SearchTerm" />

MAUI XAML:

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

Uno Platform XAML:

<Button AutomationProperties.AutomationId="submit-order" Content="Place Order" />
选择器反模式
// BAD: Tied to CSS implementation
await page.ClickAsync(".MuiButton-root.MuiButton-containedPrimary");

// BAD: Tied to DOM structure
await page.ClickAsync("div > form > div:nth-child(3) > button");

// BAD: Tied to dynamic content
await page.ClickAsync($"text=Order #{orderId}");

// GOOD: Stable test identifier
await page.ClickAsync("[data-testid='submit-order']");

// GOOD: Accessibility-driven (Playwright)
await page.GetByRole(AriaRole.Button, new() { Name = "Place Order" }).ClickAsync();

异步等待策略

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

等待策略决策树
Is the element already in the DOM?
|
+-- YES --> Is it visible and actionable?
|           |
|           +-- YES --> Interact immediately
|           +-- NO  --> Wait for visibility/enabled state
|
+-- NO  --> Wait for element to appear in DOM
            |
            Is it loaded via network request?
            |
            +-- YES --> Wait for network idle or specific API response
            +-- NO  --> Wait for render cycle to complete
特定框架的等待模式

Playwright(基于浏览器):

// Auto-waiting: Playwright waits for actionability by default
await page.ClickAsync("[data-testid='submit']"); // waits until visible + enabled

// Explicit wait for network-loaded content
await page.WaitForResponseAsync(
    response => response.Url.Contains("/api/orders") && response.Status == 200);

// Wait for element state
await page.Locator("[data-testid='results']")
    .WaitForAsync(new() { State = WaitForSelectorState.Visible });

// Wait for specific text content
await Expect(page.Locator("[data-testid='status']")).ToHaveTextAsync("Completed");

bUnit(Blazor 组件测试):

// Wait for async state changes to render
var cut = RenderComponent<OrderList>();

// Wait for component to finish async operations
cut.WaitForState(() => cut.Instance.Orders.Count > 0,
    timeout: TimeSpan.FromSeconds(5));

// Wait for specific markup
cut.WaitForAssertion(() =>
    Assert.NotEmpty(cut.FindAll("[data-testid='order-row']")),
    timeout: TimeSpan.FromSeconds(5));
等待反模式
// BAD: Hardcoded delay -- slow and still flaky
await Task.Delay(3000);
await page.ClickAsync("[data-testid='results']");

// BAD: Polling with Thread.Sleep
while (!element.IsVisible)
{
    Thread.Sleep(100); // blocks thread, no timeout safety
}

// GOOD: Framework-native wait
await page.Locator("[data-testid='results']")
    .WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = 5000 });

// GOOD: Assertion with retry (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");

    // Scope to specific component
    var form = Page.Locator("[data-testid='order-form']");
    var results = await Page.RunAxe(new AxeRunOptions
    {
        // Focus on WCAG 2.1 AA rules
        RunOnly = new RunOnlyOptions
        {
            Type = "tag",
            Values = ["wcag2a", "wcag2aa", "wcag21aa"]
        }
    });

    // Report violations with details for debugging
    foreach (var violation in results.Violations)
    {
        // Log: violation.Id, violation.Description, violation.Nodes
    }
    Assert.Empty(results.Violations);
}
UI 测试无障碍检查清单
检查项测试方法工具
颜色对比度自动化 axe-core 规则Deque.AxeCore.Playwright
键盘导航使用 Tab 键遍历所有交互元素Playwright page.Keyboard
ARIA 标签验证是否存在 aria-label / aria-labelledbyPlaywright 定位器 + 断言
焦点管理验证焦点是否移动到对话框/模态框Playwright page.Locator(':focus')
屏幕阅读器文本验证 aria-live 区域是否更新手动检查 + 对 ARIA 属性进行断言
键盘导航测试示例
[Fact]
public async Task OrderForm_TabOrder_FollowsLogicalSequence()
{
    await Page.GotoAsync("/orders/new");

    // Tab through form fields and verify focus order
    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();

    // Verify Enter submits the form
    await Page.Keyboard.PressAsync("Tab"); // focus submit button
    await Expect(Page.Locator("[data-testid='submit-order']")).ToBeFocusedAsync();
}

关键原则

  • 对于包含多个测试的 UI 测试套件,使用页面对象模型。 前期投入的成本很快就能通过减少维护工作收回。
  • 优先使用 data-testid 或基于无障碍特性的选择器,而不是 CSS 或 DOM 结构选择器。 稳定的选择器是避免测试不稳定的最有效方法。
  • 绝不要使用 Thread.Sleep 或 Task.Delay 作为等待策略。 使用框架原生等待,在超时时间内轮询条件。
  • 将无障碍检查纳入标准测试套件, 不要将其作为单独的审计。及早发现违规问题可避免积累无障碍技术债。
  • 尽可能让页面对象与框架无关。 这些模式(POM、选择器策略、等待模式)具有通用性;Playwright、bUnit 和 Appium 之间只有驱动程序 API 不同。

Agent 常见注意事项

  1. 未经团队同意,不要在生产代码中添加 data-testid 属性。 有些团队会在生产构建中移除这些属性;有些团队则会保留。请先确认项目约定。
  2. 不要在 Playwright 测试中使用 WaitForTimeout(硬编码延迟)。 它会掩盖时序问题,并拖慢测试。请改用 WaitForSelectorAsync、Expect(...).ToBeVisibleAsync() 或 WaitForResponseAsync。
  3. 在列表加载完成前,不要断言元素数量。 如果组件尚未完成渲染,FindAll("[data-testid='row']").Count 会返回 0。请先使用 WaitForState 或 WaitForAssertion。
  4. 不要以“这不是必需项”为由跳过无障碍测试。 WCAG 合规性日益成为法律要求。自动化检查几乎无需成本,就能发现容易解决的问题。
  5. 不要创建深度嵌套的页面对象。 如果页面对象中还包含页面对象,请扁平化层级。一级组件组合(页面 -> 组件)就足够了。

参考资料

评论

支持 Markdown