dotnet-ui-testing-core について
Blazor、MAUI、Uno Platform を使う .NET 開発者向けの、UI テストパターンのガイドです。保守しやすいページオブジェクト、安定したセレクター、非同期待機、アクセシビリティ検査を扱います。
主な機能
- 01ページオブジェクトモデル
- 02信頼性の高い要素セレクター戦略
- 03条件に基づく非同期待機
- 04自動アクセシビリティ検査
- 05複数の .NET UI フレームワーク向けパターン
活用シーン
- 01保守しやすい UI テストスイートの構成
- 02不安定なセレクターによるテストの不安定化を抑制
- 03非同期レンダリングやネットワーク応答の待機
- 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 テストの最大の原因です。
セレクターの優先順位(信頼性の高い順)
| 優先順位 | セレクターの種類 | 例 | 信頼性 |
|---|---|---|---|
| 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">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 だけです。
エージェントが陥りやすい問題
- チームの合意なしに、製品コードへ
data-testid属性を追加しないでください。 製品ビルドでこれらを削除するチームもあれば、残すチームもあります。まずプロジェクトの慣例を確認してください。 - Playwright のテストで
WaitForTimeout(ハードコードされた遅延)を使用しないでください。 タイミングの問題を隠し、テストを遅くします。代わりにWaitForSelectorAsync、Expect(...).ToBeVisibleAsync()、またはWaitForResponseAsyncを使用してください。 - リストが読み込まれるのを待たずに、要素数をアサートしないでください。 コンポーネントのレンダリングが完了していない場合、
FindAll("[data-testid='row']").Countはゼロを返します。まずWaitForStateまたはWaitForAssertionを使用してください。 - 「要件ではない」という理由で、アクセシビリティテストを省略しないでください。 WCAG 準拠は、ますます法的要件になっています。自動チェックを使えば、コストをほとんどかけずに、まず取り組むべき基本的な問題を検出できます。
- 階層が深くなりすぎたページオブジェクトを作成しないでください。 ページオブジェクトの中にさらにページオブジェクトがある場合は、階層をフラット化してください。コンポーネントの合成は 1 階層(ページ -> コンポーネント)で十分です。