単体テスト

ヒント

この内容は電子ブック『.NET MAUI を使用したエンタープライズ アプリケーション パターン』からの抜粋です。これは .NET Docs で閲覧することも、無料の PDF をダウンロードしてオフラインで読むこともできます。

電子ブック『.NET MAUI を使用したエンタープライズ アプリケーション パターン』の表紙のサムネイル。

マルチプラットフォーム アプリでは、デスクトップと Web ベースの両方のアプリケーションに似た問題が発生します。 モバイル ユーザーは、デバイス、ネットワークの接続性、サービスの可用性、その他のさまざまな要因が異なります。 したがって、マルチプラットフォーム アプリの品質、信頼性、およびパフォーマンスを向上させるには、実際の世界で使用されるのと同じようにテストする必要があります。 単体テスト、統合テスト、ユーザー インターフェイス テストなど、多くの種類のテストをアプリで実行する必要があります。 単体テストは最も一般的な形式であり、高品質のアプリケーションを構築するために不可欠です。

単体テストでは、アプリの小さな単位 (通常はメソッド) を受け取り、コードの残りの部分から分離し、期待どおりに動作することを確認します。 その目的は、各機能単位が期待どおりに動作することを確認し、エラーがアプリ全体に波及しないようにすることです。 バグが発生した箇所でそれを検出する方が、二次的なエラーの発生箇所で間接的にバグの影響を観察するよりも効率的です。

単体テストは、ソフトウェア開発ワークフローの構成要素になったときに、コードの品質に最大の効果をもたらします。 単体テストは、アプリケーションの設計ドキュメントと機能仕様の役割を果たします。 メソッドが記述されたらすぐに、標準、境界、不正な入力データのケースに応答してメソッドの動作を検証し、コードによって行われた明示的または暗黙的な前提条件を確認する単体テストを記述する必要があります。 または、テスト駆動型開発では、単体テストはコードの前に記述されます。 テスト駆動型開発とその実装方法の詳細については、「 チュートリアル: テスト エクスプローラーを使用したテスト駆動型開発」を参照してください。

注

単体テストはリグレッションの防止に非常に効果的です。 つまり、以前は動作していた機能が、不具合のあるアップデートによって正常に動作しなくなったということです。

単体テストでは通常、arrange-act-assert パターンが使用されます。

手順 説明
Arrange オブジェクトを初期化し、テスト対象のメソッドに渡されるデータの値を設定します。
アクション 必要な引数を指定して、テスト対象のメソッドを呼び出します。
Assert テスト対象のメソッドの操作が予測どおりに動作することを検証します。

このパターンにより、単体テストが読み取り可能で、自己記述性があり、一貫性があることが確認されます。

テスト フレームワークとプラットフォームを選択する

テスト フレームワークとは、テストを作成する際に使用する属性、アサーション、テストのライフサイクルを定義するものです。 テストプラットフォームは、これらのテストを検出して実行し、IDE、コマンドライン、継続的インテグレーション(CI)パイプラインと統合します。 VSTest と Microsoft.Testing.Platform (MTP) はテストプラットフォームであり、MSTest などのフレームワークに取って代わるものではありません。

次のフレームワークは、プラットフォームに依存しない .NET MAUI アプリケーション ロジックのテストをサポートしています。

フレームワーク フレームワークのドキュメント MTP の設定
MSTest MSTest の概要 と MSTest の使用を開始する。 MSTest を使用したテストの実行とMSTest SDK。
NUnit C# と NUnit を使い始める。 NUnit ランナー。
xUnit.net C# と xUnit.net を使ってみましょう xUnit.net v3 には、MTP サポートが搭載されています。 xUnit.net v2 には、xUnit.net または Microsoft によってサポートされていない第三者製の連携が必要です。
TUnit TUnit のドキュメント。 TUnit は MTP 上に構築されており、VSTest をサポートしていません。

MTP は実行可能アプリケーションとしてテストを実行します。 CLI、IDE、および CI のセットアップについては、「 MTP テストの実行とデバッグ」を参照してください。 .NET 10 SDK 以降では、dotnet testでglobal.jsonの MTP モードを選択できます。 このモードを使用するには、MTP 1.7 以降が必要であり、選択したすべてのテストプロジェクトが MTP に対応している必要があります。 VSTest ベースのプロジェクトと MTP ベースのプロジェクトを、同じテストの実行で混在させないでください。 プラットフォームごとに、使用するコマンドライン オプションも異なります。 詳細については、dotnet test を使用したテストを参照してください。

.NET MAUI のテスト範囲を理解する

開発マシンで単体テストを実行するには、ビジネスロジック、バリデーション、およびビューモデルをプラットフォーム固有の依存関係から切り離してください。 方法の1つは、アプリとテスト プロジェクトの両方が参照する .NET クラス ライブラリにそのコードを置くことです。 あるいは、.NET MAUI アプリケーションまたはクラス ライブラリを構成して、net10.0 など、プラットフォームに依存しない適切なターゲットを公開します。 アプリ プロジェクトの場合、そのターゲットは実行可能ファイルではなくライブラリを生成する必要があります。 ネイティブの起動コードとプラットフォーム固有の実装は、そのターゲットには含めないでください。 プロジェクトの構成については、.NET の単体テストMAUIを参照してください。

プラットフォーム固有ではないテスト プロジェクトでは、ネイティブ .NET MAUI アプリケーション コンテキストは作成されません。 アプリケーションロジックをテストする際は、ナビゲーション、UI ディスパッチ、およびデバイスサービスへの依存関係をテストダブルに置き換えてください。 これらのテストでは、ネイティブ レンダリングやデバイスのアクセス許可、あるいは置き換えるサービスの実装内容については検証されません。

プラットフォームの API またはネイティブ コントロール ハンドラーを必要とするコードをテストするには、互換性のあるプラットフォーム テスト ホストまたはデバイス ランナーを使用します。 MTP はプラットフォーム アプリケーションでホストできますが、デスクトップ テスト プロジェクトに MTP を選択しても、デバイス テスト アプリになるわけではありません。 デバイス ランナーおよびプラットフォームのテスト オプション (バージョン要件を含む) については、.NET MAUI 単体テストを参照してください。 デプロイ済みのアプリの操作を自動化するには、Appium を使用した UI テストを参照してください。 サポートしているプラットフォームでこれらのテストを実行してください。

依存性注入と単体テスト

テスト対象のコードを独立させるために、コンストラクター インジェクションを使用して、依存関係をテストダブルに置き換えます。 フェイクやモックといったテストダブルは、実際のサービスと同じインターフェースを実装しますが、テストのために制御された動作を提供します。 アーキテクチャに関するガイダンスについては、依存性の注入を参照してください。

たとえば、eShop アプリの OrderDetailViewModel は、コンストラクター経由で IAppEnvironmentService、INavigationService、ISettingsService の依存関係を受け取ります。 実際の Web サービスまたはデバイス API を呼び出すのではなく、テストで使用する依存関係に対してテスト実装を提供します。

eShop のカタログ ビュー モデルのテストでは、完全なセットアップが示されています。具体的には、モック サービスを使用して AppEnvironmentService を構築し、それらのサービスを選択した上で、モックナビゲーションサービスとともにビューモデルに渡しています。 完全なコンストラクターと初期化コードについては、保守されているサンプル テストを使用してください。 サービス依存関係を置き換えることでアプリケーション ロジックは分離できますが、テストが呼び出す他のコードにあるネイティブの要件がなくなるわけではありません。

MVVM アプリケーションのテスト

モデルとビュー モデルにネイティブ アプリケーション コンテキストが必要ない場合は、他の.NET クラスに使用するのと同じフレームワークと手法を使用してテストします。 モデル クラスやビューのモデル クラスで一般的なパターンの中には、特定の単体テスト手法を活用することで効果が高まるものがあります。

ヒント

各単体テストで 1 つの動作をテストします。 テストの再現性を確保し、各テストを独立させ、問題の特定を容易にするため、各テストの対象を 1 つの課題に限定してください。 詳細については、.NET を使用した単体テストのベスト プラクティスを参照してください。

1つの単体テストでユニットの動作の複数の側面をテストしようとしないでください。 これを行うと、テストの読み取りと更新が困難になります。 また、失敗を解釈するときに混乱を招く可能性もあります。

eShop マルチプラットフォーム アプリは MSTest を使用します。 MSTest では、テストクラスには TestClass を、各テスト メソッドには TestMethod を適用します。 属性、シグネチャ、ライフサイクルに関するガイダンスについては、MSTest を使用したテストの作成を参照してください。

異なる入力で同じテストを実行するには、インライン データの場合は TestMethod と DataRow を組み合わせ、メンバーから提供されたデータの場合は DynamicData を組み合わせます。 これらの属性はテスト ケースを提供するものであり、独立したテスト フレームワークや TestMethod の代替手段ではありません。 レガシ DataSource属性は、最新の.NETや.NET MAUI プロジェクトではなく、.NET Framework でのみ使用できます。 サポートされているオプションについては、 MSTest でのデータドリブン テストを参照してください。

非同期機能のテスト

ビュー モデルでは、非同期のサービス操作が頻繁に呼び出されます。 中間状態ではなく結果を検証するには、アサートを行う前に操作が完了するのを待ちます。 MSTest 3.3 以降を使用して、Task または ValueTask を返す非同期の MSTest メソッドを作成します。 async void のテスト メソッドは使用しないでください。 サポートされているシグネチャについては、MSTest を使用してテストを記述するを参照してください。

たとえば、eShop のカタログビュー モデルのテストにおける ProductsPropertyIsNotNullAfterViewModelInitializationTest では、モック サービスを含むビュー モデルを作成し、InitializeAsync を待機させ、Products が null ではないことをアサートしています。 このテストでは、実際のカタログ Web サービスの動作ではなく、制御されたデータを用いた初期化がチェックされます。 アプリの初期化とナビゲーションフローについては、ナビゲーションを参照してください。

非同期コマンドの場合は、結果を確認する前に、そのコマンドの非同期実行 API が完了するのを待機します。 ICommand.Executeのみを呼び出しても、非同期処理が完了したとは限りません。 MVVM Toolkit コマンド API については、「 AsyncRelayCommand」を参照してください。

INotifyPropertyChanged 実装のテスト

INotifyPropertyChanged インターフェイスを使用すると、ビューはモデルとビューモデルの変更に反応できます。 通知をテストするには、イベント ハンドラーを PropertyChangedにアタッチし、プロパティを変更して、ハンドラーが予期される PropertyNameを受け取っていることをアサートします。 関連のないプロパティ通知を成功として扱わない。

eShop の検証ビュー モデルにおけるテストの SettingForenamePropertyShouldRaisePropertyChanged テストでは、このパターンが実証されています。 Forename オブジェクトの PropertyChanged イベントを登録し、Forename.Value を設定して、Value に関する通知があるかどうかを確認します。

このテストでは、通知コントラクトを検証します。 XAML バインディングまたはネイティブ コントロールが正しく更新されることは検証しないため、それらの動作は UI テストでカバーしてください。

メッセージベースの通信のテスト

メッセージ ベースの通信を行う場合は、MVVM Toolkit の IMessenger (例: WeakReferenceMessenger) を使用してください。 メッセージング コントラクトと API の例については、疎結合コンポーネント間の通信 と MVVM Toolkit Messenger を参照してください。

発行元をテストするには、予期されるメッセージの種類の受信者を登録し、操作を実行して、受信者が予想されるペイロードを受け取っていることをアサートします。 受信者をテストするには、既知のメッセージを送信し、結果のビュー モデルの状態をアサートします。 アサーションを行う前に、非同期コマンドの完了を待ってください。

メッセージ テストを分離するには、テストごとに messenger インスタンスを作成し、テスト対象のコードに同じインスタンスを挿入します。 コードで WeakReferenceMessenger.Default が使用されている場合、クリーンアップ時にテスト受信者の登録を解除して、並行して実行されるテストが共有メッセンジャーの状態に影響を与えないようにします。 弱参照では、次のテストの前に受信者が回収されることが保証されません。 クリーンアップと分離のオプションについては、 MSTest テストのライフサイクル と テスト実行の制御に関するページを参照してください。

注

MessagingCenter は.NET 10 では非推奨です。 新しいコードの場合は、代わりに MVVM Toolkit Messenger を使用してください。

例外処理のテスト

無効なアクションまたは入力によって例外がスローされることを確認するには、フレームワークの例外アサーションを使用します。 MSTest 3.8 以降では、 Assert.Throws<T> は指定された例外の種類または派生型を受け入れますが、 Assert.ThrowsExactly<T> には正確な型が必要です。 非同期コードの場合は、非同期処理を同期アサーションに渡すのではなく、Assert.ThrowsAsync<T> または Assert.ThrowsExactlyAsync<T> を await します。 例やバージョン固有の API については、MSTest アサーションを参照してください。

例外アサーションは、期待される例外が検出された場合に成功とみなされます。この例外は、処理されていないテストの失敗ではありません。 テスト対象の操作がネイティブ ビューを構築するか、プラットフォーム ハンドラーを使用する場合でも、適切なアプリケーション コンテキストが必要です。 例外のアサーションでは、そのようなコンテキストは得られません。

ヒント

例外メッセージ文字列を調べる単体テストは記述しないでください。 例外メッセージ文字列は時間の経過と同時に変化する可能性があるため、その存在に依存する単体テストは脆弱と見なされます。

検証テスト

検証規則と、 ValidatableObject<T>によって公開される状態の両方をテストします。 各ルールについて、有効入力、無効入力、および境界値入力を含めます。 アプリで使用される検証モデルについては、「 検証」を参照してください。

eShop の検証ビュー モデルによるテストでは、MockViewModel にモック化されたナビゲーション サービスを提供し、その Validate メソッドを実行します。 CheckValidationPassesWhenBothPropertiesHaveDataTest は、両方の名前にデータがある場合に検証が成功することを確認します。 CheckValidationFailsWhenOnlyForenameHasDataTest は、姓が入力されていない場合に検証が失敗することを確認します。

Value、IsValid、Errors のプロパティと、全体的な結果を確認します。 たとえば、名にのみデータがある場合、名はエラーなしで有効なままにする必要があり、姓はエラーで無効である必要があります。 これらのアサーションは、UI にエラーが正しく表示されるかどうかではなく、バリデーションロジックを検証します。

まとめ

単体テストでは、アプリの小さな単位 (通常はメソッド) を対象とし、コードの残りの部分から分離し、期待どおりに動作することを確認します。 その目的は、各機能単位が期待どおりに動作することを確認し、エラーがアプリ全体に波及しないようにすることです。

アプリケーション ロジックを分離するには、サービスの依存関係をテスト ダブルに置き換えます。 テスト フレームワークと互換性のあるテスト プラットフォームを選択し、それぞれでサポートされている属性やアサーションを使用してください。

プラットフォーム非依存のモデルとビュー モデルを、通常の .NET 単体テストでテストします。 実際のサービスまたはネイティブ アプリケーション コンテキストに依存する動作については、デバイス、プラットフォーム、統合、UI テストを使用します。