mcpField
D
dotnet-ui-testing-core

dotnet-ui-testing-core

github.com/wshaddixテスト・品質8684 回インストール35 週間前に更新
リポジトリ

dotnet-ui-testing-core について

Blazor、MAUI、Uno Platform を使う .NET 開発者向けの、UI テストパターンのガイドです。保守しやすいページオブジェクト、安定したセレクター、非同期待機、アクセシビリティ検査を扱います。

主な機能

  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 は、それぞれのスキルが扱います。bUnit については [skill:dotnet-blazor-testing]、Appium/XHarness については [skill:dotnet-maui-testing]、Uno WASM テストについては [skill:dotnet-uno-testing] を参照してください。ブラウザー自動化の詳細は [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 を返し、テスト作成者がアプリケーションのフローに沿ってテストできるようにします。
  • ページオブジェクトから生のセレクターを公開しないでください。 テストでは ClickAsync("#submit") ではなく、LoginAsync() を呼び出します。
  • アサーションはページオブジェクトではなく、テスト内に記述します。 ページオブジェクトはデータ(例: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-labelledby が設定されていることを確認Playwright のロケーターとアサーション
フォーカス管理ダイアログやモーダルにフォーカスが移動することを確認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 テストスイートでは、ページオブジェクトモデルを使用します。 最初にかかるコストは、メンテナンスの削減によってすぐに回収できます。
  • CSS や DOM 構造に基づくセレクターより、data-testid またはアクセシビリティに基づくセレクターを優先します。 安定したセレクターは、不安定なテストを防ぐ最も効果的な方法です。
  • 待機方法として Thread.Sleep や Task.Delay は決して使用しません。 タイムアウト付きで条件をポーリングする、フレームワーク標準の待機機能を使用します。
  • アクセシビリティチェックは別個の監査ではなく、標準のテストスイートの一部として実行します。 違反を早期に検出することで、アクセシビリティに関する負債を防げます。
  • 可能な限り、ページオブジェクトをフレームワークに依存しない形に保ちます。 パターン(POM、セレクター戦略、待機パターン)は共通で、Playwright、bUnit、Appium 間で変わるのはドライバー API だけです。

エージェントが陥りやすい問題

  1. チームの合意なしに、製品コードへ data-testid 属性を追加しないでください。 製品ビルドでこれらを削除するチームもあれば、残すチームもあります。まずプロジェクトの慣例を確認してください。
  2. Playwright のテストで WaitForTimeout(ハードコードされた遅延)を使用しないでください。 タイミングの問題を隠し、テストを遅くします。代わりに WaitForSelectorAsync、Expect(...).ToBeVisibleAsync()、または WaitForResponseAsync を使用してください。
  3. リストが読み込まれるのを待たずに、要素数をアサートしないでください。 コンポーネントのレンダリングが完了していない場合、FindAll("[data-testid='row']").Count はゼロを返します。まず WaitForState または WaitForAssertion を使用してください。
  4. 「要件ではない」という理由で、アクセシビリティテストを省略しないでください。 WCAG 準拠は、ますます法的要件になっています。自動チェックを使えば、コストをほとんどかけずに、まず取り組むべき基本的な問題を検出できます。
  5. 階層が深くなりすぎたページオブジェクトを作成しないでください。 ページオブジェクトの中にさらにページオブジェクトがある場合は、階層をフラット化してください。コンポーネントの合成は 1 階層(ページ -> コンポーネント)で十分です。

参考資料

コメント