エンタープライズ アプリの単体テスト

Note

この電子ブックは 2017 年春に発行されて以降、改訂されていません。 このブックには今なお価値のある内容が多く含まれていますが、一部の記載内容は古くなっています。

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

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

単体テストは、ソフトウェア開発ワークフローの不可欠な部分となっている場合に、コードの品質に最大の効果をもたらします。 メソッドが記述されたらすぐに、標準、境界、不正な入力データのケースに応答してメソッドの動作を検証し、コードによって行われた明示的または暗黙的な前提条件を確認する単体テストを記述する必要があります。 または、テスト駆動型開発では、単体テストはコードの前に記述されます。 このシナリオでは、単体テストは設計ドキュメントと機能仕様の両方として機能します。

Note

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

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

  • 単体テスト メソッドの arrange セクションでは、オブジェクトを初期化し、テスト対象のメソッドに渡されるデータの値を設定します。
  • act セクションでは、必要な引数を使用して、テスト対象のメソッドを呼び出します。
  • assert セクションでは、テスト対象のメソッドの操作が予測どおりに動作することを検証します。

このパターンに従うことで、単体テストを確実に読みやすく一貫性があるものにすることができます。

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

疎結合アーキテクチャを採用する動機の 1 つが、単体テストが容易になることです。 Autofac に登録されている型の 1 つが OrderService クラスです。 次のコード例は、このクラスのアウトラインを示しています。

public class OrderDetailViewModel : ViewModelBase  
{  
    private IOrderService _ordersService;  

    public OrderDetailViewModel(IOrderService ordersService)  
    {  
        _ordersService = ordersService;  
    }  
    ...  
}

OrderDetailViewModel クラスは、OrderDetailViewModel オブジェクトのインスタンスを作成するときにコンテナーが解決する IOrderService 型に依存します。 ただし、OrderDetailViewModel クラスを単体テストする OrderService オブジェクトを作成するのではなく、テストの目的で OrderService オブジェクトをモックに置き換えます。 図 10-1 は、この関係を示しています。

Classes that implement the IOrderService interface

図 10-1: IOrderService インターフェイスを実装するクラス

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

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

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

ヒント

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

eShopOnContainers モバイル アプリは単体テストを実行し、異なる 2 種類の単体テストをサポートします。

  • ファクトは常に true となるテストで、不変条件をテストします。
  • セオリーは特定のデータ セットに対してのみ true となるテストです。

eShopOnContainers モバイル アプリに含まれる単体テストはファクト テストであるため、各単体テスト メソッドは [Fact] 属性で修飾されます。

Note

xUnit テストはテスト ランナーによって実行されます。 テスト ランナーを実行するには、必要なプラットフォームの eShopOnContainers.TestRunner プロジェクトを実行します。

非同期機能のテスト

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

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

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

    Assert.NotNull(orderViewModel.Order);  
}

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

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

INotifyPropertyChanged 実装のテスト

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

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

[Fact]  
public async Task SettingOrderPropertyShouldRaisePropertyChanged()  
{  
    bool 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.True(invoked);  
}

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

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

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

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

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

    Assert.True(messageReceived);  
}

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

例外処理のテスト

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

[Fact]  
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 メソッドを呼び出した結果に関するテストが必要です。

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

    bool isValid = mockViewModel.Validate();  

    Assert.True(isValid);  
}

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

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

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

    bool isValid = mockViewModel.Validate();  

    Assert.False(isValid);  
    Assert.NotNull(mockViewModel.Forename.Value);  
    Assert.Null(mockViewModel.Surname.Value);  
    Assert.True(mockViewModel.Forename.IsValid);  
    Assert.False(mockViewModel.Surname.IsValid);  
    Assert.Empty(mockViewModel.Forename.Errors);  
    Assert.NotEmpty(mockViewModel.Surname.Errors);  
}

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

まとめ

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

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

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