mcpField
M
maui-unit-testing

maui-unit-testing

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

maui-unit-testing について

.NET MAUIのユニットテスト設定、ViewModelのテストパターン、よくある問題を扱うスキルです。MAUIのプラットフォームAPIを注入可能なインターフェースの背後に置き、ViewModelをテストしやすくする方法を示します。

主な機能

  1. 01テスト対象フレームワークと出力設定の案内
  2. 02MAUI APIを扱う注入可能なインターフェース
  3. 03ViewModelの状態とコマンドのテスト
  4. 04MAUIサービスのモック方針
  5. 05非同期処理とCIテストのヒント

活用シーン

  1. 01MAUIアプリのユニットテストを設定
  2. 02UIを起動せずにViewModelをテスト
  3. 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 → INavigationService
  • Application.Current → 使用自体を避ける
  • SecureStorage.Default → ISecureStorage
  • Connectivity.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 サービスモック戦略
ISecureStorageMock<ISecureStorage> — GetAsync/SetAsync をスタブ化
IPreferencesMock<IPreferences> — Get/Set/Remove をスタブ化
IConnectivityMock<IConnectivity> — NetworkAccess を返す
IGeolocationMock<IGeolocation> — 固定の Location を返す
IFilePickerMock<IFilePicker> — FileResult を返す
IMediaPickerMock<IMediaPicker> — FileResult を返す
Shell のナビゲーションINavigationService の背後に抽象化する
IDispatcherDispatch をスタブ化し、アクションを同期的に実行する

アーキテクチャ:テスト容易性を高めるインターフェイス優先の設計

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 が成功する

コメント