次の方法で共有


単体テスト

ヒント

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

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

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

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

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

注意

単体テストは回帰に対して非常に効果的です。 つまり、以前は機能していたが、障害のある更新によって妨げられている機能です。

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

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

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

依存関係の挿入と単体テスト

疎結合アーキテクチャを採用する動機の 1 つが、単体テストが容易になることです。 依存関係の挿入サービスに登録されている型の 1 つが IAppEnvironmentService インターフェイスです。 次のコード例は、このクラスのアウトラインを示しています。

public class OrderDetailViewModel : ViewModelBase
{
    private IAppEnvironmentService _appEnvironmentService;

    public OrderDetailViewModel(
        IAppEnvironmentService appEnvironmentService,
        IDialogService dialogService, INavigationService navigationService, ISettingsService settingsService)
        : base(dialogService, navigationService, settingsService)
    {
        _appEnvironmentService = appEnvironmentService;
    }
}

OrderDetailViewModel クラスは、OrderDetailViewModel オブジェクトをインスタンス化するときに依存関係の挿入コンテナーが解決する IAppEnvironmentService 型に依存します。 ただし、実際のサーバー、デバイス、構成を利用して OrderDetailViewModel クラスを単体テストする IAppEnvironmentService オブジェクトを作成するのではなく、テストの目的で IAppEnvironmentService オブジェクトをモック オブジェクトに置き換えます。 モック オブジェクトは、オブジェクトまたはインターフェイスの同じシグネチャを持つものですが、単体テストに役立つ特定の方法で作成されます。 多くの場合、依存関係の挿入と共に使用され、さまざまなデータとワークフロー シナリオをテストするためにインターフェイスの特定の実装が提供されます。

このアプローチでは、実行時に IAppEnvironmentService オブジェクトを OrderDetailViewModel クラスに渡すことができます。また、テストの容易性のため、テスト時にモック クラスを OrderDetailViewModel クラスに渡すことができます。 このアプローチの主な利点は、ランタイム プラットフォーム機能、Web サービス、データベースなどの扱いにくいリソースを必要とせずに単体テストを実行できる点です。

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

MVVM アプリケーションからのモデルとビュー モデルのテストは、他のクラスのテストと同じであり、同じツールと手法を使用します。これには、単体テストやモックなどの機能が含まれます。 ただし、モデルとビュー モデルのクラスで一般的な一部のパターンでは、特定の単体テスト手法のベネフィットを得ることができます。

ヒント

各単体テストでテストするのは 1 つのことだけにしてください。 テストの複雑さが増すと、そのテストの検証がより困難になります。 単体テストを 1 つの懸念事項に制限することで、テストの再現性が高くなり、分離され、実行時間が短縮されます。 その他のベスト プラクティスについては、.NET での単体テストのベスト プラクティスに関する記事をご覧ください。

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

eShop マルチプラットフォーム アプリでは、2 種類の単体テストをサポートする MSTest を使って単体テストを実行します。

テストの種類 属性 説明
TestMethod TestMethod 実行する実際のテスト メソッドを定義します。
DataSource DataSource 特定のデータ セットに対してのみ true となるテスト。

eShop マルチプラットフォーム アプリに含まれる単体テストは TestMethod であるため、各単体テスト メソッドは TestMethod 属性で修飾されます。 MSTest に加えて、NUnitxUnit など、他にもいくつかのテスト フレームワークを利用できます。

非同期機能のテスト

MVVM パターンを実装する場合、ビュー モデルは通常、サービスに対して (多くの場合は非同期的に) 操作を呼び出します。 通常、これらの操作を呼び出すコードのテストでは、実際のサービスの代わりにモックが使用されます。 次のコード例は、モック サービスをビュー モデルに渡すことで非同期機能をテストする方法を示しています。

[TestMethod]
public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest()
{
    // Arrange
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    // Act
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    // Assert
    Assert.IsNotNull(orderViewModel.Order);
}

この単体テストでは、InitializeAsync メソッドの呼び出し後に OrderDetailViewModel インスタンスの Order プロパティに値が設定されることを確認します。 InitializeAsync メソッドは、ビュー モデルの対応するビューの移動時に呼び出されます。 ナビゲーションの詳細については、「ナビゲーション」を参照してください。

OrderDetailViewModel インスタンスが作成されるときに、IOrderService インスタンスが引数として指定されることが期待されます。 ただし、OrderService は Web サービスからデータを取得します。 したがって、OrderDetailViewModel コンストラクターの引数として、OrderService クラスのモック バージョンである OrderMockService インスタンスが指定されます。 その後、IOrderService の操作が使用されるビュー モデルの InitializeAsync メソッドが呼び出されると、Web サービスと通信するのではなく、モック データが取得されます。

INotifyPropertyChanged 実装のテスト

INotifyPropertyChanged インターフェイスを実装すると、ビューが、ビュー モデルとモデルから発生した変更に対応できるようになります。 これらの変更は、コントロールに表示されるデータに限定されるものではなく、アニメーションの開始やコントロールの無効化を引き起こすビュー モデルの状態など、ビューの制御にも使用されます。

単体テストで直接更新できるプロパティは、イベント ハンドラーを PropertyChanged イベントにアタッチし、プロパティの新しい値を設定した後でイベントが発生するかどうかを確認することでテストできます。 次のコード例は、そのようなテストを示します。

[TestMethod]
public async Task SettingOrderPropertyShouldRaisePropertyChanged()
{
    var invoked = false;
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    orderViewModel.PropertyChanged += (sender, e) =>
    {
        if (e.PropertyName.Equals("Order"))
            invoked = true;
    };
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    Assert.IsTrue(invoked);
}

この単体テストでは、OrderViewModel クラスの InitializeAsync メソッドが呼び出され、その Order プロパティが更新されます。 Order プロパティに対して PropertyChanged イベントが発生した場合、単体テストは合格します。

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

次のコード例に示すように、疎結合クラス間の通信に MessagingCenter クラスを使用するビュー モデルは、テスト対象のコードによって送信されるメッセージをサブスクライブすることによって単体テストできます。

[TestMethod]
public void AddCatalogItemCommandSendsAddProductMessageTest()
{
    var messageReceived = false;
    var catalogService = new CatalogMockService();
    var catalogViewModel = new CatalogViewModel(catalogService);

    MessagingCenter.Subscribe<CatalogViewModel, CatalogItem>(
        this, MessageKeys.AddProduct, (sender, arg) =>
    {
        messageReceived = true;
    });
    catalogViewModel.AddCatalogItemCommand.Execute(null);

    Assert.IsTrue(messageReceived);
}

この単体テストは、実行中の AddCatalogItemCommand に応答して CatalogViewModelAddProduct メッセージを発行することを確認します。 MessagingCenter クラスはマルチキャスト メッセージ サブスクリプションをサポートしているため、単体テストは AddProduct メッセージをサブスクライブし、その受信に応答してコールバック デリゲートを実行できます。 ラムダ式として指定されたこのコールバック デリゲートは、テストの動作を確認するために Assert ステートメントによって使用されるブール型フィールドを設定します。

例外処理のテスト

次のコード例に示すように、無効なアクションまたは入力に対して特定の例外がスローされることを確認する単体テストを記述することもできます。

[TestMethod]
public void InvalidEventNameShouldThrowArgumentExceptionText()
{
    var behavior = new MockEventToCommandBehavior
    {
        EventName = "OnItemTapped"
    };
    var listView = new ListView();

    Assert.Throws<ArgumentException>(() => listView.Behaviors.Add(behavior));
}

この単体テストでは、ListView コントロールに OnItemTapped という名前のイベントがないため、例外がスローされます。 Assert.Throws<T> メソッドは、予期される例外の型が T であるジェネリック メソッドです。 Assert.Throws<T> メソッドに渡される引数は、例外をスローするラムダ式です。 したがって、ラムダ式が ArgumentException をスローする場合、単体テストは合格します。

ヒント

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

検証のテスト

検証の実装のテストには、検証規則が正しく実装されていることをテストする、そして ValidatableObject<T> クラスが期待どおりに実行されることをテストするという 2 つの側面があります。

通常、検証ロジックはテストが簡単です。これは通常、出力が入力に依存する自己完結型のプロセスであるためです。 次のコード例に示すように、少なくとも 1 つの検証規則が関連付けられた各プロパティで Validate メソッドを呼び出した結果に関するテストが必要です。

[TestMethod]
public void CheckValidationPassesWhenBothPropertiesHaveDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";
    mockViewModel.Surname.Value = "Smith";

    var isValid = mockViewModel.Validate();

    Assert.IsTrue(isValid);
}

この単体テストでは、MockViewModel インスタンス内の 2 つの ValidatableObject<T> プロパティに両方のデータがある場合に検証が成功することを確認します。

検証が成功したことを確認するだけでなく、検証単体テストでは、クラスが期待どおりに実行されることを確認するために、各 ValidatableObject<T> インスタンスの ValueIsValid、および Errors のプロパティの値も確認する必要があります。 次のコード例は、これを行う単体テストを示しています。

[TestMethod]
public void CheckValidationFailsWhenOnlyForenameHasDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";

    bool isValid = mockViewModel.Validate();

    Assert.IsFalse(isValid);
    Assert.IsNotNull(mockViewModel.Forename.Value);
    Assert.IsNull(mockViewModel.Surname.Value);
    Assert.IsTrue(mockViewModel.Forename.IsValid);
    Assert.IsFalse(mockViewModel.Surname.IsValid);
    Assert.AreEqual(mockViewModel.Forename.Errors.Count(), 0);
    Assert.AreNotEqual(mockViewModel.Surname.Errors.Count(), 0);
}

この単体テストでは、MockViewModelSurname プロパティにデータがなく、各 ValidatableObject<T> インスタンスの ValueIsValid、および Errors プロパティが正しく設定されている場合に検証が失敗します。

まとめ

単体テストでは、アプリの小さな単位 (通常はメソッド) を受け取り、コードの残りの部分から分離し、期待どおりに動作することを確認します。 その目的は、機能の各ユニットが期待どおりに実行されることを確認することです。そのため、エラーはアプリ全体に伝達されません。

テスト対象のオブジェクトの動作は、依存オブジェクトの動作をシミュレートするモック オブジェクトに依存オブジェクトを置き換えることで分離できます。 これにより、ランタイム プラットフォーム機能、Web サービス、データベースなどの扱いにくいリソースを必要とせずに単体テストを実行できるようになります。

MVVM アプリケーションからのモデルとビュー モデルのテストは、他のクラスのテストと同じであり、同じツールと手法を使用できます。