maui-unit-testing について
.NET MAUIのユニットテスト設定、ViewModelのテストパターン、よくある問題を扱うスキルです。MAUIのプラットフォームAPIを注入可能なインターフェースの背後に置き、ViewModelをテストしやすくする方法を示します。
主な機能
- 01テスト対象フレームワークと出力設定の案内
- 02MAUI APIを扱う注入可能なインターフェース
- 03ViewModelの状態とコマンドのテスト
- 04MAUIサービスのモック方針
- 05非同期処理とCIテストのヒント
活用シーン
- 01MAUIアプリのユニットテストを設定
- 02UIを起動せずにViewModelをテスト
- 03dotnet testで回帰テストを実行
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ davidortinau/ maui-skills --skill maui-unit-testingインストール後、Claude Code で/maui-unit-testingを入力して読み込まれたか確認します。
同梱ファイルreferences/unit-testing-api.md
詳細
.NET MAUI 単体テスト — 注意点とベストプラクティス
プロジェクトテンプレート、xUnit の例、ViewModel のテストパターン、CLI コマンドについては、references/unit-testing-api.md を参照してください。
⚠️ TFM の注意点:プラットフォーム固有の TFM は使用しないでください
<TargetFramework>net9.0-ios</TargetFramework>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
⚠️ OutputType の注意点:アプリプロジェクトをテストの依存関係として使用する場合
テストプロジェクトがアプリプロジェクトを参照していると、テスト TFM 用に Exe としてビルドしようとするため、ビルドに失敗します。条件付きの OutputType を追加してください。
<OutputType>Exe</OutputType>
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'net9.0'">
<OutputType>Exe</OutputType>
</PropertyGroup>
よくある間違い
❌ ViewModel で MAUI の静的 API を使用する
MAUI の静的 API を直接呼び出す ViewModel はテストできません。
// ❌ Untestable — Shell.Current requires a running MAUI app
public async Task GoToDetail(int id)
=> await Shell.Current.GoToAsync($"detail?id={id}");
// ✅ Inject an interface — fully testable
public class MyViewModel(INavigationService nav)
{
public async Task GoToDetail(int id)
=> await nav.GoToAsync($"detail?id={id}");
}
インターフェイスの背後にラップする静的 API:
Shell.Current→INavigationServiceApplication.Current→ 使用自体を避けるSecureStorage.Default→ISecureStorageConnectivity.Current→IConnectivity
❌ ViewModel の状態ではなく UI バインディングを検証する
// ❌ Testing the binding — fragile, needs a running UI
Assert.Equal("Hello", label.Text);
// ✅ Testing the ViewModel — fast, no platform dependency
Assert.Equal("Hello", viewModel.Title);
Assert.True(viewModel.SaveCommand.CanExecute(null));
MAUI サービスのモック戦略
| MAUI サービス | モック戦略 |
|---|---|
ISecureStorage | Mock<ISecureStorage> — GetAsync/SetAsync をスタブ化 |
IPreferences | Mock<IPreferences> — Get/Set/Remove をスタブ化 |
IConnectivity | Mock<IConnectivity> — NetworkAccess を返す |
IGeolocation | Mock<IGeolocation> — 固定の Location を返す |
IFilePicker | Mock<IFilePicker> — FileResult を返す |
IMediaPicker | Mock<IMediaPicker> — FileResult を返す |
| Shell のナビゲーション | INavigationService の背後に抽象化する |
IDispatcher | Dispatch をスタブ化し、アクションを同期的に実行する |
アーキテクチャ:テスト容易性を高めるインターフェイス優先の設計
ViewModel が MAUI のプラットフォーム依存関係を一切持たないように、サービスインターフェイスを定義します。
// ✅ These make your entire ViewModel layer testable
public interface INavigationService
{
Task GoToAsync(string route);
Task GoBackAsync();
}
public interface IDialogService
{
Task<bool> ConfirmAsync(string title, string message);
}
MauiProgram.cs に実装を登録し、ViewModel にインターフェイスを注入します。
ヒント
- ViewModel で
Application.CurrentやShell.Currentを使用しないでください — 注入可能なサービスでラップします ObservableCollection<T>と[ObservableProperty](MVVM Toolkit)を使用して、テスト可能な状態を管理します- UI バインディングではなく、ViewModel のプロパティと
CanExecuteを検証してください TaskCompletionSourceを使用して、非同期の待機処理をテストします- CI で
dotnet testを実行して、リグレッションを早期に検出します - 実機テスト:プラットフォームの実際の API(センサー、カメラ、Bluetooth)には
xunit.runner.devicesを使用します
チェックリスト
- テストプロジェクトのターゲットは通常の
net9.0/net10.0(プラットフォーム固有の TFM ではない) - アプリプロジェクトにテスト TFM 用の条件付き
OutputTypeがある - すべての MAUI 静的 API が注入可能なインターフェイスの背後にラップされている
- ViewModel は UI バインディングではなく、プロパティとコマンドを通じてテストされている
- テストでは
IDispatcherがモック化され、同期的に実行される - CI で
dotnet testが成功する
コメント
Markdown 対応