次の方法で共有


MVVM

MVVM でテストが容易なプレゼンテーション層を作成する

Brent Edwards

コード サンプルのダウンロード

Windows フォーム時代の従来のアプリで標準的だったテスト方法は、ビューをレイアウトし、ビューの分離コードでコードを作成してから、アプリをテストとして実行することでした。さいわい、事態はその後少し進歩しています。

Windows Presentation Foundation (WPF) の登場によって、データ バインディングという概念はまったく新しい段階に進化しました。その結果生まれたのが、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) という新しい設計パターンです。MVVM を使えば、プレゼンテーション ロジックを実際のプレゼンテーションから分離できます。つまり基本的に、ほとんどの場合はビューの分離コードでコードを作成する必要がありません。

これは、テストが容易なアプリの開発に関心を持つ方にとって大きな強化点です。現在では、プレゼンテーション ロジックをビューの分離コードにアタッチする (その結果、ビュー自体のライフサイクルによってテストが複雑になる) のではなく、単純な従来の CLR オブジェクト (POCO) を使用できます。ビューモデルには、ビューのようなライフサイクルの制約がありません。単体テストでビューモデルのインスタンスを作成して、テストできます。

今回は、MVVM を使ってアプリ用のテストが容易なプレゼンテーション層を作成する方法を紹介します。手法を説明しやすいよう、ここでは私が作成した Charmed というオープン ソース フレームワークのサンプル コードと、Charmed Reader という付属サンプル アプリを利用します。フレームワークとサンプル アプリは GitHub (github.com/brentedwards/Charmed、英語) から入手できます。

Charmed フレームワークについては、2013 年 7 月号の記事 (msdn.microsoft.com/magazine/dn296512) で、Windows 8 のフレームワークとサンプル アプリとして紹介しました。続いて 2013 年 9 月号の記事 (msdn.microsoft.com/magazine/dn385706) では、これを Windows 8 と Windows Phone 8 のフレームワークとサンプル アプリとして、クロスプラットフォーム対応にする方法について説明しました。どちらの記事でも、アプリをテストが容易な状態に保つために下した判断について説明しました。今回は、これらの判断を振り返りながら、実際にアプリをテストする方法を紹介します。今回は、例として Windows 8 と Windows Phone 8 のコードを使用しますが、ここで説明する概念と手法はあらゆる種類のアプリに応用できます。

サンプル アプリについて

テストが容易なプレゼンテーション層の作成方法を紹介するためのサンプル アプリは、Charmed Reader という名前です。Charmed Reader は、Windows 8 と Windows Phone 8 の両方で機能するシンプルなブログ リーダー アプリで、ここで取り上げる重要なポイントを説明するために必要な最小限の機能を備えています。クロスプラットフォームに対応し、どちらのプラットフォームでもほぼ同様に動作しますが、Windows 8 アプリの場合は Windows 8 固有の機能を使用します。アプリは簡単ですが、単体テストを行ううえで十分な機能を備えています。

単体テストとは

単体テストの背景にある考え方は、個別のコード チャンク (単体) に注目し、期待する方法でコードを使用するテスト メソッドを作成して、期待どおりの結果を得られるかどうかテストするというものです。このテスト コードを実行するには、いずれかのテスト ハーネス フレームワークを使用します。Visual Studio 2012 で機能するテスト ハーネス フレームワークは、いくつかあります。サンプル コードでは、Visual Studio 2012 以前のバージョンに組み込まれている MSTest を使用します。目的は、単一の単体テスト メソッドの対象を特定のシナリオに絞ることです。メソッドやプロパティで対応できると期待しているすべてのシナリオに対処するには、複数の単体テスト メソッドが必要な場合もあります。

単体テスト メソッドは、他の開発者が理解しやすいように、一定の形式に従っている必要があります。一般にベスト プラクティスと見なされている形式は、次のとおりです。

  1. Arrange
  2. Act
  3. Assert

まず、テスト対象のクラスのインスタンスと必要に応じてその依存関係を作成するために必要な、設定コードが考えられます。これが単体テストの Arrange セクションです。

単体テストで、実際のテストの準備が完了したら、テスト対象のメソッドやプロパティを実行できます。これは、テストの Act セクションです。Arrange セクションで (必要に応じて) 設定したパラメーターを指定して、テスト対象のメソッドやプロパティを実行できます。

最後に、テスト対象のメソッドやプロパティを実行したら、メソッドやプロパティで行ったはずの処理が正しく行われたことを検証する必要があります。これが、テストの Assert セクションです。Assert 段階では、アサート メソッドを呼び出して、実際の結果と期待する結果を比較します。実際の結果が期待どおりだった場合、単体テストは成功です。期待どおりでなかった場合、テストは失敗です。

このベスト プラクティスの形式に従って私が作成するテストは、通常次のようになります。

[TestMethod]
 public void SomeTestMethod()
 {
   // Arrange
   // *Insert code to set up test
   // Act
   // *Insert code to call the method or property under test
   // Assert
   // *Insert code to verify the test completed as expected
 }

開発者によっては、この形式を使用する際に、テストの各セクション (Arrange/Act/Assert) を示すコメントを含めない人もいます。しかし私は、実際のテスト対象や単に設定しているだけかどうかを見失わないよう、3 つのセクションをコメントで区切るようにしています。

適切に作成した単体テストの包括的なスイートには、アプリの "生きた" ドキュメントとして機能するという利点もあります。初めて目にするコードでも、単体テストの対象となっているさまざまなシナリオを調べれば、期待されているコードの使用方法を把握できます。

テスト容易性のための計画

テストが容易なアプリを作成する場合は、事前に計画すると非常に効果的です。アプリのアーキテクチャを設計することになるので、単体テストに役に立ちます。静的メソッド、シール クラス、データベース アクセス、および Web サービス呼び出しは、どれもがアプリの単体テストを困難または不可能にする要因です。しかし、計画を立てておけば、アプリに及ぶ影響を最小限に抑えられます。

Charmed Reader アプリは、ブログ記事を読むためのアプリです。これらのブログ記事をダウンロードするには RSS フィードに Web 経由でアクセスする必要があるので、単体テストがかなり難しい場合があります。まず、単体テストは迅速に、しかも接続されていない状態で実行できる必要があります。単体テストで Web へのアクセスを使用すると、このような原則に違反する可能性があります。

さらに、単体テストは繰り返し可能である必要があります。一般に、ブログは定期的に更新されるため、同じデータをいつまでもダウンロードし続けられるとは限りません。私は、事前に計画しなければ、ブログ記事の読み込み機能に対する単体テストを実行できないことを認識していました。

実行する必要があると私が認識していた処理は、次のとおりです。

  1. MainViewModel で、ユーザーが読もうとしているすべてのブログ記事を一度に読み込む。
  2. 読み込んだブログ記事を、ユーザーが保存したさまざまな RSS フィードからダウンロードする。
  3. ダウンロードしたブログ記事を、データ転送オブジェクト (DTO) として解析し、ビューに提供する。

RSS フィードをダウンロードするコードを MainViewModel に配置すると、MainViewModel の役割が突如として、データを読み込むこととビューでそのデータにバインドして表示できるようにすることだけではなくなります。その結果、Web 要求と XML データの解析も MainViewModel の役割になります。MainViewModel の本来の役割は、Web 要求を行って XML データを解析する、ヘルパーの呼び出しです。ヘルパーの呼び出し後に、MainViewModel には、表示するブログ記事を表すオブジェクトのインスタンスを提供する必要があります。このようなオブジェクトを DTO と呼びます。

これを把握していれば、MainViewModel から呼び出せるヘルパー オブジェクトに RSS フィードの読み込みと解析を抽象化できます。ただし、これで終わりではありません。RSS フィード データを処理するヘルパー クラスを作成するだけの場合、この機能について MainViewModel を対象に作成するあらゆる単体テストでも、最終的にこのヘルパー クラスを呼び出して Web にアクセスすることになります。既に述べたように、これは単体テストの目的に反します。そこで、もう 1 歩踏み込む必要があります。

RSS フィード データ読み込み機能のインターフェイスを作成すると、ビューモデルが具象クラスではなくインターフェイスと対話するようにできます。このようにすれば、アプリの代わりに単体テストを実行するときのために、インターフェイスの別の実装を提供できます。これが、モック作成の背景にある概念です。実際にアプリを実行する場合は、本物の RSS フィード データを読み込む本物のオブジェクトが必要です。単体テストを実行する場合は、RSS データを読み込んだように見せかけるだけで実際には Web にアクセスしない、モック オブジェクトが必要になります。モック オブジェクトを使用すれば、反復可能で変化しない一貫したデータを作成できます。そのため、毎回期待されている項目を単体テストで正確に把握できます。

これを念頭に置くと、ブログ記事を読み込むインターフェイスは次のようになります。

public interface IRssFeedService
 {
   Task> GetFeedsAsync();
 }

メソッドは GetFeedsAsync だけです。このメソッドを使用して MainViewModel でブログ記事のデータを読み込むことができます。MainViewModel では、IRssFeedService によるデータ読み込み方法やデータ解析方法を認識する必要はありません。MainViewModel で認識する必要があるのは、GetFeedsAsync の呼び出しがブログ記事データを非同期で返すことだけです。これは、アプリがクロスプラットフォーム対応であることを考えると特に重要です。

Windows 8 と Windows Phone 8 では、RSS フィード データをダウンロードして解析する方法が異なります。ここではブログ フィードを直接ダウンロードする代わりに、IRssFeedService インターフェイスを作成して MainViewModel でこのインターフェイスと対話することで、MainViewModel に同じ機能の実装をいくつも用意する必要をなくします。

依存関係の挿入を利用すると、MainViewModel に IRssFeedService の正しいインスタンスを適切なタイミングで提供できます。上述のように、単体テスト時は IRssFeedService のモック インスタンスを提供します。単体テストについて説明する前提として、Windows 8 と Windows Phone 8 のコードの利用に関する興味深い点は、これらのプラットフォームには現在利用できる実際の動的モック作成フレームワークがないことです。モック作成はコードの単体テスト方法を大きく左右する部分なので、モックを作成するシンプルな方法を考え出す必要がありました。考案した RssFeedServiceMock を図 1 に示します。

図 1 RssFeedServiceMock

public class RssFeedServiceMock : IRssFeedService
 {
   public Func> GetFeedsAsyncDelegate { get; set; }
   public Task> GetFeedsAsync()
   {
     if (this.GetFeedsAsyncDelegate != null)
     {
       return Task.FromResult>(this.GetFeedsAsyncDelegate());
     }
     else
     {
       return Task.FromResult>(null);
     }
   }
 }

基本的な目標は、データの読み込み方法を設定できるデリゲートを提供可能にすることです。Windows 8 または Windows Phone 8 用に開発している場合以外は、Moq、Rhino Mocks、NSubstitute などの動的モック作成フレームワークを使用できる可能性が十分にあります。独自のモックを作成する場合も動的モック作成フレームワークを使用する場合も、同じ原則を適用できます。

これで、IRssFeedService インターフェイスを作成して MainViewModel に挿入し、MainViewModel で IRssFeedService インターフェイスの GetFeedsAsync を呼び出し、RssFeedServiceMock を作成して使用できる状態になりました。そこで、次は MainViewModel と IRssFeedService の対話の単体テストを実行します。この対話の重要なテスト項目は、MainViewModel で GetFeedsAsync を適切に呼び出した結果として返されるフィード データが、MainViewModel で FeedData プロパティを使用して利用できるようにするフィード データと同一であることです。図 2の単体テストは、この点を検証しています。

図 2 フィード読み込み機能のテスト

[TestMethod]
 public void FeedData()
 {
   // Arrange
   var viewModel = GetViewModel();
   var expectedFeedData = new List();
   this.RssFeedService.GetFeedsAsyncDelegate = () =>
     {
       return expectedFeedData;
     };
   // Act
   var actualFeedData = viewModel.FeedData;
   // Assert
   Assert.AreSame(expectedFeedData, actualFeedData);
 }

ビューモデル (または関連するその他のオブジェクト) の単体テストを実行するときはいつも、私はテスト対象のビューモデルの実際のインスタンスを提供する、ヘルパー メソッドを利用するのが好きです。ビューモデルは時間と共に変化しがちなので、ビューモデルにはさまざまな要素、つまりさまざまなコンストラクター パラメーターを挿入している可能性があります。すべての単体テストでビューモデルの新しいインスタンスを作成する場合にコンストラクターのシグネチャを変更すると、すべての単体テストを変更する必要も生じます。一方、ビューモデルの新しいインスタンスを作成するヘルパー メソッドを記述する場合は、1 か所だけ変更すれば十分です。この例では、GetViewModel がヘルパー メソッドです。

private MainViewModel GetViewModel()
 {
   return new MainViewModel(this.RssFeedService, 
     this.Navigator, this.MessageBus);
 }

また、TestInitialize 属性を使って、毎回テストを実行する前に MainViewModel の依存関係が再作成されるようにします。これは、次に示す TestInitialize メソッドで行います。

[TestInitialize]
 public void Init()
 {
   this.RssFeedService = new RssFeedServiceMock();
   this.Navigator = new NavigatorMock();
   this.MessageBus = new MessageBusMock();
 }

このようにすると、このテスト クラスのすべての単体テストに、実行時にすべてのモックの新しいインスタンスが作成されます。

テスト自体に話を戻すと、次のコードでは、期待するフィード データを作成し、そのフィードを返すモック RSS フィード サービスを設定しています。

var expectedFeedData = new List();
 this.RssFeedService.GetFeedsAsyncDelegate = () =>
   {
     return expectedFeedData;
   };

実際の FeedData インスタンスは必要ないため、expectedFeedData のリストには一切追加していないことに注意してください。ここでの要件は、MainViewModel の最終的な処理対象がリスト自体になることだけです。実際にリストに FeedData インスタンスが存在する場合に実行される処理については、少なくともこのテストでは考慮しません。

テストの Act 部分には、次のコードが含まれています。

var actualFeedData = viewModel.FeedData;

これで、actualFeedData が expectedFeedData の同じインスタンスであるとアサートできます。同じインスタンスでない場合、MainViewModel が機能しなかったことになり、単体テストは失敗します。

Assert.AreSame(expectedFeedData, actualFeedData);

テストが容易なナビゲーション

サンプル アプリで重要なもう 1 つのテスト対象は、ナビゲーションです。今回はビューとビューモデルを分離する必要があるため、Charmed Reader サンプル アプリではビューモデルに基づくナビゲーションを使用します。Charmed Reader はクロスプラットフォーム アプリであり、ここで作成するビューモデルは両方のプラットフォームで使用しますが、ビューは Windows 8 と Windows Phone 8 で異なっている必要があります。理由は多数ありますが、簡単に述べれば、各プラットフォームの XAML がわずかに異なっているためです。このため、ビューモデルがビューを認識して処理が複雑になることは、望ましくありませんでした。

いくつかの理由から、インターフェイスの背後にあるナビゲーション機能を抽象化することが解決策となりました。最大の理由は、各プラットフォームにはナビゲーションに関連するさまざまなクラスが含まれており、ビューモデルがプラットフォームによる違いに対処する必要をなくす必要があったことです。また、どちらのプラットフォームの場合も、ナビゲーションに関連するクラスのモックを作成できません。そこで、これらの問題をビューモデルから抽象化して、INavigator インターフェイスを作成しました。

public interface INavigator
 {
   bool CanGoBack { get; }
   void GoBack();
   void NavigateToViewModel(object parameter = null);
 #if WINDOWS_PHONE
   void RemoveBackEntry();
 #endif // WINDOWS_PHONE
 }

コンストラクターを通じて INavigator を MainViewModel に挿入する一方、MainViewModel では ViewFeed という名前のメソッドで INavigator を使用します。

public void ViewFeed(FeedItem feedItem)
 {
   this.navigator.NavigateToViewModel(feedItem);
 }

ViewFeed での INavigator との対話方法を見ると、単体テストを作成する際に次の 2 つの点を確認する必要があることがわかります。

  1. ViewFeed に渡される FeedItem が、NavigateToViewModel に渡される FeedItem と同じである。
  2. NavigateToViewModel に渡されるビューモデル型が、FeedItemViewModel である。

実際にテストを作成するには、INavigator 用に別のモックを作成する必要があります。図 3 は、INavigator 用のモックを示しています。実際のメソッドを呼び出したときにテスト コードを実行する方法として、各メソッドで先ほどと同じデリゲートを使用したパターンを採用しました。ここでも、モック作成フレームワークをサポートしているプラットフォームで開発している場合、独自にモックを作成する必要はありません。

図 3 INavigator 用のモック

public class NavigatorMock : INavigator
 {
   public bool CanGoBack { get; set; }
   public Action GoBackDelegate { get; set; }
   public void GoBack()
   {
     if (this.GoBackDelegate != null)
     {
       this.GoBackDelegate();
     }
   }
   public Action NavigateToViewModelDelegate { get; set; }
   public void NavigateToViewModel(object parameter = null)
   {
     if (this.NavigateToViewModelDelegate != null)
     {
       this.NavigateToViewModelDelegate(typeof(TViewModel), parameter);
     }
   }
 #if WINDOWS_PHONE
   public Action RemoveBackEntryDelegate { get; set; }
   public void RemoveBackEntry()
   {
     if (this.RemoveBackEntryDelegate != null)
     {
       this.RemoveBackEntryDelegate();
     }
   }
 #endif // WINDOWS_PHONE
 }

モックの Navigator クラスが完成したら、単体テストで使用できます (図 4 参照)。

図 4 モックの Navigator を使ったナビゲーションのテスト

[TestMethod]
 public void ViewFeed()
 {
   // Arrange
   var viewModel = this.GetViewModel();
   var expectedFeedItem = new FeedItem();
   Type actualViewModelType = null;
   FeedItem actualFeedItem = null;
   this.Navigator.NavigateToViewModelDelegate = (viewModelType, parameter) =>
     {
       actualViewModelType = viewModelType;
       actualFeedItem = parameter as FeedItem;
     };
   // Act
   viewModel.ViewFeed(expectedFeedItem);
   // Assert
   Assert.AreSame(expectedFeedItem, actualFeedItem, "FeedItem");
   Assert.AreEqual(typeof(FeedItemViewModel), 
     actualViewModelType, "ViewModel Type");
 }

このテストで実際に考慮する点は、正しい FeedItem が渡され、ナビゲートされているビューモデルが適切であることです。モックを扱う場合は、特定のテストについて考慮が必要な点と考慮が不要な点を念頭に置くことが重要です。このテストでは、MainViewModel が対話する INavigator インターフェイスがあるので、ナビゲーションが実際に発生するかどうかを考慮する必要はありません。ナビゲーションの発生は、実行時のインスタンス用に INavigator を実装しているすべての要素で処理されます。考慮が必要なのは、ナビゲーションの発生時に INavigator に適切なパラメーターが提供されることだけです。

テストが容易なセカンダリ タイル

テストの対象とする最後の部分は、セカンダリ タイルです。セカンダリ タイルは Windows 8 と Windows Phone 8 のどちらでも使用でき、セカンダリ タイルを使うとユーザーはアプリの要素をホーム画面にピン留めして、アプリの特定の部分へのディープ リンクを作成できます。ただし、セカンダリ タイルの処理方法は 2 つのプラットフォームでまったく異なるため、プラットフォーム固有の実装を提供する必要があります。このような違いはありますが、両方のプラットフォームで使用可能な、セカンダリ タイル用の一貫したインターフェイスを提供できます。

public interface ISecondaryPinner
 {
   Task Pin(TileInfo tileInfo);
   Task Unpin(TileInfo tileInfo);
   bool IsPinned(string tileId);
 }

TileInfo クラスは、セカンダリ タイルを作成するために両方のプラットフォームのプロパティを組み合わせた DTO です。各プラットフォームで使用する TileInfo のプロパティの組み合わせが異なるため、プラットフォームごとに別の方法でテストする必要があります。Windows 8 バージョンを詳しく見てみましょう。図 5 は、ビューモデルが ISecondaryPinner を使う方法を示しています。

図 5 の Pin メソッドでは、2 つの処理を実行しています。1 つ目は、セカンダリ タイルの実際のピン留めで、2 つ目は、FeedItem のローカル ストレージへの保存です。そのため、この 2 つの処理をテストする必要があります。Pin メソッドは FeedItem のピン留めを試みた結果に基づいてビューモデルの IsFeedItemPinned プロパティを変更するため、ISecondaryPinner の Pin メソッドで考えられる 2 つの結果 (true の場合と false の場合) をテストする必要もあります。図 6は、成功のシナリオをテストする、私が実装した最初のテストを示しています。

図 5 ISecondaryPinner の使用

public async Task Pin(Windows.UI.Xaml.FrameworkElement anchorElement)
 {
   // Pin the feed item, then save it locally to make sure it's still available
   // when they return.
   var tileInfo = new TileInfo(
     this.FormatSecondaryTileId(),
     this.FeedItem.Title,
     this.FeedItem.Title,
     Windows.UI.StartScreen.TileOptions.ShowNameOnLogo |
       Windows.UI.StartScreen.TileOptions.ShowNameOnWideLogo,
     new Uri("ms-appx:///Assets/Logo.png"),
     new Uri("ms-appx:///Assets/WideLogo.png"),
     anchorElement,
     Windows.UI.Popups.Placement.Above,
     this.FeedItem.Id.ToString());
   this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
   if (this.IsFeedItemPinned)
   {
     await SavePinnedFeedItem();
   }
 }

図 6 ピン留め成功のテスト

[TestMethod]
 public async Task Pin_PinSucceeded()
 {
   // Arrange
   var viewModel = GetViewModel();
   var feedItem = new FeedItem
   {
     Title = Guid.NewGuid().ToString(),
     Author = Guid.NewGuid().ToString(),
     Link = new Uri("https://www.bing.com")
   };
   viewModel.LoadState(feedItem, null);
   Placement actualPlacement = Placement.Default;
   TileInfo actualTileInfo = null;
   SecondaryPinner.PinDelegate = (tileInfo) =>
     {
       actualPlacement = tileInfo.RequestPlacement;
       actualTileInfo = tileInfo;
       return true;
     };
   string actualKey = null;
   List actualPinnedFeedItems = null;
   Storage.SaveAsyncDelegate = (key, value) =>
     {
       actualKey = key;
       actualPinnedFeedItems = (List)value;
     };
   // Act
   await viewModel.Pin(null);
   // Assert
   Assert.AreEqual(Placement.Above, actualPlacement, "Placement");
   Assert.AreEqual(string.Format(Constants.SecondaryIdFormat,
     viewModel.FeedItem.Id), actualTileInfo.TileId, "Tile Info Tile Id");
   Assert.AreEqual(viewModel.FeedItem.Title,
     actualTileInfo.DisplayName, "Tile Info Display Name");
   Assert.AreEqual(viewModel.FeedItem.Title,
     actualTileInfo.ShortName, "Tile Info Short Name");
   Assert.AreEqual(viewModel.FeedItem.Id.ToString(),
     actualTileInfo.Arguments, "Tile Info Arguments");
   Assert.AreEqual(Constants.PinnedFeedItemsKey, actualKey, "Save Key");
   Assert.IsNotNull(actualPinnedFeedItems, "Pinned Feed Items");
 }

このテストの設定は、前のテストよりも少し多くなっています。まず、コントローラーの後で、FeedItem インスタンスを設定します。Title と Author の両方について Guid の ToString を呼び出していることに注意してください。これは、実際の値は関係なく、Assert セクションで比較できる値があるかどうかだけを考慮しているためです。Link は Uri で、テストが機能するには適切な Uri が必要になるため、この段階で用意しました。ここでも、実際の Uri は関係なく、Uri が有効かどうかだけを考慮しています。設定の残りの部分では、Assert セクションで比較するために、ピン留めと保存の操作を取得します。このコードが成功のシナリオを実際にテストすることを確認するうえで重要なポイントは、PinDelegate が true を返し、成功を示すことです。

図 7に示すテストは先ほどのテストとほとんど同じですが、失敗のシナリオを対象としています。PinDelegate が false を返すことで、テストの対象を失敗のシナリオにしています。失敗のシナリオでは、SaveAsync が呼び出されていないことも Assert セクションで確認する必要があります。

図7 ピン留め失敗のテスト

[TestMethod]
 public async Task Pin_PinNotSucceeded()s
 {
   // Arrange
   var viewModel = GetViewModel();
   var feedItem = new FeedItem
   {
     Title = Guid.NewGuid().ToString(),
     Author = Guid.NewGuid().ToString(),
     Link = new Uri("https://www.bing.com")
   };
   viewModel.LoadState(feedItem, null);
   Placement actualPlacement = Placement.Default;
   TileInfo actualTileInfo = null;
   SecondaryPinner.PinDelegate = (tileInfo) =>
   {
     actualPlacement = tileInfo.RequestPlacement;
     actualTileInfo = tileInfo;
     return false;
   };
   var wasSaveCalled = false;
   Storage.SaveAsyncDelegate = (key, value) =>
   {
     wasSaveCalled = true;
   };
   // Act
   await viewModel.Pin(null);
   // Assert
   Assert.AreEqual(Placement.Above, actualPlacement, "Placement");
   Assert.AreEqual(string.Format(Constants.SecondaryIdFormat,
     viewModel.FeedItem.Id), actualTileInfo.TileId, "Tile Info Tile Id");
   Assert.AreEqual(viewModel.FeedItem.Title, actualTileInfo.DisplayName,
     "Tile Info Display Name");
   Assert.AreEqual(viewModel.FeedItem.Title, actualTileInfo.ShortName,
     "Tile Info Short Name");
   Assert.AreEqual(viewModel.FeedItem.Id.ToString(),
     actualTileInfo.Arguments, "Tile Info Arguments");
   Assert.IsFalse(wasSaveCalled, "Was Save Called");
 }

テストが容易なアプリを作成することは、簡単ではありません。特に、ユーザー操作がかかわるプレゼンテーション層をテストすることは困難です。テストが容易なアプリを作成しようとしていることを前もって認識していれば、あらゆる段階でテストの容易性を優先して判断を下せます。また、アプリのテストを難しくする要因にも注意を向けることができ、解決方法を考え出せます。

ここまでの 3 回にわたる連載では、MVVM パターンを使ってテストが容易なアプリを作成する方法について、特に Windows 8 と Windows Phone 8 を対象に説明してきました。最初の記事では、そのままではテストが難しい Windows 8 固有の機能を活用しながらもテストが容易な Windows 8 アプリを作成する方法を説明しました。2 回目の記事では、Windows 8 と Windows Phone 8 のテストが容易なクロスプラットフォーム アプリを開発することに話を進めました。今回の記事では、テストを容易にすることを第一に考えて工夫したアプリのテスト手法を説明しました。

MVVM は、さまざまな解釈がある幅広いトピックです。このような興味深いトピックに関する私の解釈をご紹介できたことを、うれしく思います。MVVM の使用には、特にテスト容易性に関して大きな意義を感じています。また、テスト容易性の追求は刺激的で有益な活動でもあります。テストが容易なアプリを作成するための私の方法を知っていただき、ありがとうございました。

Brent Edwards は、マイクロソフトのスタックおよびモバイル アプリ開発に重点を置いているカスタム アプリ開発会社 Magenic の主任コンサルタントです。ミネアポリスの Twin Cities Windows 8 User Group の共同設立者でもあります。彼の連絡先は、brente@magenic.com(英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Jason Bock (Magenic) に心より感謝いたします。
Jason Bock は Magenic (www.magenic.com、英語) のプラクティス リードで、『Metaprogramming in .NET』(www.manning.com/hazzard、英語) の共著者でもあります。彼の Web サイトは jasonbock.net(英語) です。Twitter のアカウントは @jasonbock(英語) です。