NerdDinnerチュートリアル

NerdDinnerステップ12:単体テスト

Scott Guthrie 著/Chica
2010/03/08

 本記事は、Microsoftの本社副社長であり、ASP.NETやSilverlightなどの開発チームを率いるScott Guthrie氏が公開している「NerdDinner Tutorial」を翻訳したものです。氏の許可を得て転載しています。

[これは無償の"NerdDinner"アプリケーション・チュートリアルのステップ12で、ASP.NET MVCを使用して、小さいながらも完全なWebアプリケーションを構築する手順を紹介しています。]

 NerdDinner機能を検証する自動化された単体テスト一式を開発しましょう。これにより、今後アプリケーションへの変更や改善を、自信を持って行えます。

なぜ単体テスト?

 会社への通勤中のある朝、突然作業中のアプリケーションについてひらめきます。アプリケーションが劇的に良くなるように実装できる変更点があることに気付くのです。それはコードの整理、新しい機能の追加、あるいはバグの修正といったリファクタリングかもしれません。

 コンピュータの前に着いたときに問題に直面します。“この改善を行うことがどれくらい安全なのか?” もし変更によって副作用があったり、何かを壊してしまったりしたら? その変更は単純で実装には数分しか掛からないかもしれない、でももしそのアプリケーションのすべてのケースを手動でテストするのに何時間も掛かったら? もしあるケースをカバーし忘れて、壊れたアプリケーションが製造過程に入ったら? この改善は本当にそのすべての努力に値するのか?

 自動化された単体テストは、継続的にアプリケーションを向上させ、作業中のコードに対する不安を取り除くことができるセーフティ・ネットを提供します。簡単に機能を検証できる自動化された単体テストがあれば、コードを書くことに自信が持て、そうでなければちゅうちょしそうな改善も実行する力を与えてくれます。また、より保守性があり寿命の長いソリューションの作成をサポートするので、非常に高い投資回収率が実現できます。

 ASP.NET MVCフレームワークでは、アプリケーション機能の単体テストを簡単で自然に行えます。またテスト・ファースト・ベースの開発ができるテスト駆動開発(Test Driven Development、TDD)も可能です。

NerdDinner.Testsプロジェクト

 本チュートリアルの最初でNerdDinnerアプリケーションを作成したとき、アプリケーションのプロジェクトと一緒に、単体テスト・プロジェクトを作成するかどうかのダイアログがポップアップしました。


図1

 そのまま“Yes, create a unit test project(はい、単体テスト・プロジェクトを作成します)”のラジオボタンを選択しました。これにより、“NerdDinner.Tests”プロジェクトがこのソリューションに追加されています。


図2

 NerdDinner.TestsプロジェクトはNerdDinnerアプリケーションのプロジェクトのアセンブリを参照しており、そのアプリケーションの機能性を検証する自動化されたテストを、そこへ簡単に追加できます。

Dinnerモデル・クラスへ単体テストを作成

 モデル層を構築したときに作成したDinnerクラスを検証するテストをNerdDinner.Testsプロジェクトにいくつか追加してみましょう。

 まずテスト・プロジェクト内にモデル関連のテストを保存する“Models”という新規フォルダを作成します。そして、そのフォルダを右クリックし、[追加]−[新しいテスト]メニュー・コマンドを選択します。これにより、“新しいテストの追加”ダイアログがポップアップします。

 “単体テスト”の作成を選択して、“DinnerTest.cs”という名前にします。


図3

 “OK”ボタンをクリックすると、Visual StudioはDinnerTest.csファイルをプロジェクトに追加します(そして開きます)。


図4

 デフォルトのVisual Studioの単体テスト・テンプレートは、その中に数多くの鋳型(いがた)コードを持っていますが、私には少しややこしく感じました。整理して以下のようなコードだけにしてみましょう。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {

  [TestClass]
  public class DinnerTest {

  }
}

 上記のDinnerTestクラス上にある[TestClass]属性により、テストおよび、任意のテストの初期化および後始末のコードを含むクラスとして認識されます。そこに[TestMethod]属性を持つpublicメソッドを追加して、テストを定義できます。

 以下はDinnerクラスのテスト実施のために追加する、2つのテストのうちの1つです。最初のテストは、すべてのプロパティが正しく設定されないで新しい夕食会が作成された場合に、その夕食会が無効であることを検証します。2つ目のテストは、夕食会に有効な値ですべてのプロパティが設定された場合に、その夕食会が有効であることを検証します。

[TestClass]
public class DinnerTest {

  [TestMethod]
  public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

    //Arrange
    Dinner dinner = new Dinner() {
      Title = "Test title",
      Country = "USA",
      ContactPhone = "BOGUS"
    };

    // Act
    bool isValid = dinner.IsValid;

    //Assert
    Assert.IsFalse(isValid);
  }

  [TestMethod]
  public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {

    //Arrange
    Dinner dinner = new Dinner {
      Title = "Test title",
      Description = "Some description",
      EventDate = DateTime.Now,
      HostedBy = "ScottGu",
      Address = "One Microsoft Way",
      Country = "USA",
      ContactPhone = "425-703-8072",
      Latitude = 93,
      Longitude = -92,
    };

    // Act
    bool isValid = dinner.IsValid;

    //Assert
    Assert.IsTrue(isValid);
  }
}

 上記でテスト名が非常に明示的で(ちょっとくどい)と思われるでしょう。これは最終的に何百、何千の小さいテストを作成することになるかもしれず、それぞれの意図や動作を即時に判断できるようにしておきたいからです(特にテスト・ランナーで失敗の一覧を見ているとき)。そのテスト名は、テストしている機能に基づいて付けられるべきです。上記では“Noun_Should_Verb”の名前付けパターンを使用しています。

 ここでは“AAA”(“Arrange, Act, Assert”の略)テスティング・パターンを使用してテストを構造化しています。

  • Arrange:テストされるユニットのセットアップ
  • Act:テストおよびキャプチャ結果の下で、ユニットを実施
  • Assert:動作を検証

 テストを書く際、個々のテストで多くのことをしないようにします。その代わり、各テストは1つの概念だけを検証するべきです(これにより、失敗原因の特定が非常に簡単になります)。よいガイドラインとしては、各テストで1つのアサート文だけを持たせるようにしてみることです。もし2つ以上のアサート文がテスト・メソッドにある場合、同じ概念をテストするためにそれらが使用されているかどうか確かめてください。怪しければ、別のテストを作ってください。

テストの実行

 Visual Studio 2008 Professional(および、それ以上のエディション)にはビルトインのテスト・ランナーがあり、Visual Studioの単体テスト・プロジェクトをIDEで実行する際に使用できます。[テスト]−[実行]−[ソリューションのすべてのテスト]メニュー・コマンドを選択(もしくは[Ctrl]+[R][A]をタイプ)すると、すべての単体テストを実行できます。あるいは、特定のテスト・クラスまたはテスト・メソッド内にカーソルを置き、[テスト]−[実行]−[現在のコンテキストのテスト]メニュー・コマンドを選択(もしくは[Ctrl]+[R][T]をタイプ)すると、単体テストのサブセットを実行できます。

 DinnerTestクラスにカーソルを置いて、“[Ctrl]+[R][T]”をタイプし、定義したばかりの2つのテストを実行してみましょう。これを行うと、“テスト結果”のウィンドウがVisual Studio内に表示され、その中に一覧化されたテスト実行の結果が見られます。


図5

注:VSのテスト結果ウィンドウはデフォルトでクラス名列を表示しません。テスト結果ウィンドウ内で右クリックして、[列の追加および削除]メニュー・コマンドを使えばこれを追加できます。

 この2つのテストは1秒もかからず実行され、両方とも成功したことが確認できます。次に、特定のルール検証と、Dinnerクラスに追加した2つのヘルパー・メソッド、IsUserHost()とIsUserRegisterd()をカバーするテストをさらに作成して、テストを増やします。Dinnerクラスに対して、これらすべてのテストを設定しておくと、今後非常に簡単で安全に新しいビジネス・ルールや検証を追加できるようになります。Dinnerに新しいルール・ロジックを追加して、その後、数秒内でそれが以前のロジック機能をどれも壊していないことを検証できます。

 説明的なテスト名を使用することにより、各テストが何を検証しているのか簡単に理解できるようにしている点をご確認ください。[ツール]−[オプション]メニュー・コマンドを使用し、[テスト ツール]−[テストの実行]構成画面を開き、“失敗または結果不確定の単体テストの結果をダブル・クリックした場合にテストの失敗箇所を表示する”というチェック・ボックスをチェックすることをお勧めします。これにより、テスト結果ウィンドウで失敗をダブル・クリックすると、すぐにそのアサートの失敗にジャンプします。

DinnersController単体テストの作成

 ではDinnersController機能を検証する単体テストをいくつか作成しましょう。まずテスト・プロジェクト内の“Controllers”フォルダ上で右クリックし、[追加]−[新しいテスト]メニュー・コマンドを選択します。“単体テスト”を作成して、“DinnersControllerTest.cs”という名前にします。

 DinnersController上のDetailsアクション・メソッドを検証する2つのテスト・メソッドを作成します。1つ目は、既存の夕食会がリクエストされたときにビューが返されるかどうかを検証します。2つ目は、存在しない夕食会が要求されたときに“NotFound”ビューが返されるかどうかを検証します。

[TestClass]
public class DinnersControllerTest {

  [TestMethod]
  public void DetailsAction_Should_Return_View_For_ExistingDinner() {

    // Arrange
    var controller = new DinnersController();

    // Act
    var result = controller.Details(1) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected View");
  }

  [TestMethod]
  public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

    // Arrange
    var controller = new DinnersController();

    // Act
    var result = controller.Details(999) as ViewResult;

    // Assert
    Assert.AreEqual("NotFound", result.ViewName);
  }
}

 上記のコードはクリーンにコンパイルされます。ですがテストを実行すると、両方とも失敗します。


図6

 エラー・メッセージを見てみると、失敗した理由が、DinnersRepositoryクラスがデータベースに接続できなかったからだということが分かります。NerdDinnerアプリケーションはNerdDinnerアプリケーション・プロジェクトの\App_Dataディレクトリ配下にあるローカルのSQL Server Expressファイルへの接続文字列を使用しています。NerdDinner.Testsプロジェクトがアプリケーション・プロジェクトとは別のディレクトリでコンパイルされ実行されると、その接続文字列内の相対パスの位置が間違ったものになります。

 テスト・プロジェクトに、そのSQL Server Expressデータベース・ファイルをコピーして、テスト・プロジェクトのApp.configファイルに適切なテスト用の接続文字列を追加すれば、これを修正できます。これで上記のテストはブロックされず実行できます。

 しかし、実際のデータベースを使用した単体テストのコードは数多くの課題をもたらします。特に以下のようなものです。

  • 単体テストの実行時間が大幅に遅くなります。テストの実行が長くなると、それらを頻繁に実行しなくなります。理想的には単体テストを数秒で実行できるようにし、そしてそのプロジェクトをコンパイルするのと同じくらい自然にできるようにしたいものです。
  • テスト内でのセットアップとクリーンアップのロジックが複雑化します。各単体テストは、ほかのものと隔離して(副作用や依存性のないように)独立させておきたいものです。実際のデータベースと動作させるときには、状態に注意し、テストとテストの間では、それをリセットしなければなりません。

 これらの問題点に対処し、テストで実際のデータベースを使用しないようにサポートする“依存性の注入”と呼ばれるデザイン・パターンを見てみましょう。

依存性の注入(Dependency Injection)

 現在、DinnersControllerはDinnerRepositoryクラスと密接に“対”になっています。“カップリング”とは、あるクラスが処理のために、明示的に別のクラスに依存しているような状況を意味します。

public class DinnersController : Controller {

  DinnerRepository dinnerRepository = new DinnerRepository();

  //
  // GET: /Dinners/Details/5

  public ActionResult Details(int id) {

    Dinner dinner = dinnerRepository.FindDinner(id);

    if (dinner == null)
      return View("NotFound");

    return View(dinner);
  }

 DinnerRepositoryクラスでデータベースへのアクセスが必要なため、DinnerRepository上でDinnersControllerクラスが持つ密接な対の依存性は、DinnersControllerアクション・メソッドをテストするために、データベースの用意を要求することになります。

 “依存性の注入”と呼ばれるデザイン・パターンを採用することで、これに対処できます。これは、(データアクセスを提供しているリポジトリ・クラスのような)依存性を、それらを使用しているクラス内に、暗黙的に作成されないようにするアプローチです。代わりに、依存性はコンストラクタの引数を使用して明示的にクラスに引き渡せます。もしその依存性がインターフェイスを使用して定義されているなら、単体テストのケースに対しては“偽”の依存性の実装を引き渡すという柔軟性を持たせることができます。これにより、実際にデータベースにアクセスを要求しないテスト用の依存性の実装が作成できます。

 この動作を確認するために、DinnersControllerで依存性の注入を実装しましょう。

■IDinnerRepositoryインターフェイスを抽出

 最初の手順は、夕食会を取得して更新するためにコントローラが要求するリポジトリのコントラクトをカプセル化する新しいIDinnerRepositoryインターフェイスを作成します。

 \Modelsフォルダ上で右クリックし、[追加]−[新しい項目]メニュー・コマンドを選択して、IDinnerRepository.csという名前の新しいインターフェイスを作成することで、手動でこのインターフェイスのコントラクトが定義できます。

 あるいは、既存のDinnerRepositoryクラスから自動的にインターフェイスを抽出して作成する、Visual Studio Professional(またはそれ以上のエディション)にビルトインされているリファクタリング・ツールが使用できます。これは、既存のDinnerRepositoryクラスから自動的にインターフェイスを抽出して作成します。VSを使用してこのインターフェイスを抽出するには、単純にテキスト・エディタ内でDinnerRepository上にカーソルを置き、右クリックして、[リファクタ]−[インターフェイスの抽出]メニュー・コマンドを選択します。


図7

 “インターフェイスの抽出”ダイアログを起動すると、作成するインターフェイスの名前を入力するポップアップが現れます。デフォルトではIDinnerRepositoryになっており、そのインターフェイスに追加する既存のDinnerRepositoryクラスのpublicメソッドを自動的にすべて選択しています。


図8

 “OK”ボタンをクリックすると、Visual Studioは新しいIDinnerRepositoryインターフェイスをアプリケーションに追加します。

public interface IDinnerRepository {

  IQueryable<Dinner> FindAllDinners();
  IQueryable<Dinner> FindByLocation(float latitude, float longitude);
  IQueryable<Dinner> FindUpcomingDinners();
  Dinner       GetDinner(int id);

  void Add(Dinner dinner);
  void Delete(Dinner dinner);

  void Save();
}

 そして、既存のDinnerRepositoryクラスは更新され、インターフェイスを実装しています。

public class DinnerRepository : IDinnerRepository {
   ...
}

■コンストラクタでの注入をサポートするようにDinnersControllerを更新

 新しいインターフェイスを使用するように、DinnersControllerを更新します。

 現在、DinnersControllerはハードコーディングされており、“dinnerRepository”フィールドは常にDinnerRepositoryクラスです。

public class DinnersController : Controller {

  DinnerRepository dinnerRepository = new DinnerRepository();

  ...
}

 “dinnerRepository”フィールドがDinnerRepositoryの代わりにIDinnerRepository型になるように変更します。その後、2つのpublicなDinnersControllerコンストラクタを追加します。コンストラクタの1つは、引数としてIDinnerRepositoryを引き渡せるようにします。もう1つは、既存のDinnerRepository実装を使用するデフォルトのコンストラクタです。

public class DinnersController : Controller {

  IDinnerRepository dinnerRepository;

  public DinnersController()
    : this(new DinnerRepository()) {
  }

  public DinnersController(IDinnerRepository repository) {
    dinnerRepository = repository;
  }
  ...
}

 デフォルトでは、ASP.NET MVCはデフォルトのコンストラクタを使用してクラスを作成するため、実行時のDinnersControllerは継続して、データアクセスを実行するためにDinnerRepositoryクラス使用します。

 しかし、これから単体テストを更新し、パラメータ付きのコンストラクタを使用して“偽”の夕食会のリポジトリ実装を引き渡すようにします。この“偽”の夕食会のリポジトリは実際のデータベースへのアクセスを要求せず、代わりにインメモリのサンプル・データを使用します。

■FakeDinnerRepositoryクラスの作成

 FakeDinnerRepositoryクラスを作成しましょう。

 “Fakes”ディレクトリをNerdDinner.Testsプロジェクト内に作成して、新しいFakeDinnerRepositoryクラスをそこへ追加します(フォルダ上を右クリックして[追加]−[クラス]を選択)。


図9

 コードを更新して、FakeDinnerRepositoryクラスがIDinnerRepositoryインターフェイスを実装するようにします。そのために、その上で右クリックして、“IDinnerRepositoryインターフェイスの実装”コンテキスト・メニューのコマンドを選択します。


図10

 これにより、Visual Studioは自動的にFakeDinnerRepositoryクラスにIDinnerRepositoryインターフェイスのメンバをすべて、デフォルトの“スタブ・アウト”実装により追加します。

public class FakeDinnerRepository : IDinnerRepository {

  public IQueryable<Dinner> FindAllDinners() {
    throw new NotImplementedException();
  }

  public IQueryable<Dinner> FindByLocation(float lat, float long){
    throw new NotImplementedException();
  }

  public IQueryable<Dinner> FindUpcomingDinners() {
    throw new NotImplementedException();
  }

  public Dinner GetDinner(int id) {
    throw new NotImplementedException();
  }

  public void Add(Dinner dinner) {
    throw new NotImplementedException();
  }

  public void Delete(Dinner dinner) {
    throw new NotImplementedException();
  }

  public void Save() {
    throw new NotImplementedException();
  }
}

 その後、FakeDinnerRepository実装を更新し、コンストラクタの引数として、そこへ引き渡されたインメモリのList<Dinner>コレクションで対処します。

public class FakeDinnerRepository : IDinnerRepository {

  private List<Dinner> dinnerList;

  public FakeDinnerRepository(List<Dinner> dinners) {
    dinnerList = dinners;
  }

  public IQueryable<Dinner> FindAllDinners() {
    return dinnerList.AsQueryable();
  }

  public IQueryable<Dinner> FindUpcomingDinners() {
    return (from dinner in dinnerList
        where dinner.EventDate > DateTime.Now
        select dinner).AsQueryable();
  }

  public IQueryable<Dinner> FindByLocation(float lat, float lon) {
    return (from dinner in dinnerList
        where dinner.Latitude == lat && dinner.Longitude == lon
        select dinner).AsQueryable();
  }

  public Dinner GetDinner(int id) {
    return dinnerList.SingleOrDefault(d => d.DinnerID == id);
  }

  public void Add(Dinner dinner) {
    dinnerList.Add(dinner);
  }

  public void Delete(Dinner dinner) {
    dinnerList.Remove(dinner);
  }

  public void Save() {
    foreach (Dinner dinner in dinnerList) {
      if (!dinner.IsValid)
        throw new ApplicationException("Rule violations");
    }
  }
}

 データベースを必要とせず、代わりにインメモリのDinnerオブジェクトの一覧で対処する、偽のIDinnerRepository実装ができました。

■単体テストでFakeDinnerRepositoryを使用

 先にデータベースが利用できなかったために失敗したDinnersControllerの単体テストに戻りましょう。テスト・メソッドを更新して、以下のコードのように、DinnersControllerに対してサンプルのインメモリ夕食会データをひも付けるFakeDinnerRepositoryを使用します。

[TestClass]
public class DinnersControllerTest {

  List<Dinner> CreateTestDinners() {

    List<Dinner> dinners = new List<Dinner>();

    for (int i = 0; i < 101; i++) {

      Dinner sampleDinner = new Dinner() {
        DinnerID = i,
        Title = "Sample Dinner",
        HostedBy = "SomeUser",
        Address = "Some Address",
        Country = "USA",
        ContactPhone = "425-555-1212",
        Description = "Some description",
        EventDate = DateTime.Now.AddDays(i),
        Latitude = 99,
        Longitude = -99
      };

      dinners.Add(sampleDinner);
    }

    return dinners;
  }

  DinnersController CreateDinnersController() {
    var repository = new FakeDinnerRepository(CreateTestDinners());
    return new DinnersController(repository);
  }

  [TestMethod]
  public void DetailsAction_Should_Return_View_For_Dinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Details(1);

    // Assert
    Assert.IsInstanceOfType(result, typeof(ViewResult));
  }

  [TestMethod]
  public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Details(999) as ViewResult;

    // Assert
    Assert.AreEqual("NotFound", result.ViewName);
  }
}

 いまこれらのテストを実行すると、両方が成功します。


図11

 一番よい点は、実行に1秒もかからないことと、複雑なセットアップやクリーンアップのロジックが必要ないことです。これですべてのDinnersControllerアクション・メソッドのコード(一覧、ページング、詳細、作成、更新、削除など)を実際のデータベースへ接続する必要なく、単体テストできます。

サイド・トピック:依存性の注入フレームワーク

 (上記で行ったように)依存性の注入を手動で実行してもうまく動作しますが、アプリケーションにある依存性やコンポーネントの数が増えると保守が難しくなっていきます。

 より柔軟性のある依存管理の提供をサポートしているいくつかの.NET用依存性の注入フレームワークがあります。“制御の反転”(Inversion of Control、IoC)コンテナとも呼ばれることがあるこれらのフレームワークは、実行時にオブジェクトへ依存性を指定して引き渡す際の設定を多くのレベルで可能にするメカニズムを提供します(一番よく使用されるのはコンストラクタ・インジェクション)。.NETで人気のあるOSSの依存性の注入/IOCフレームワークはAutoFac、Ninject、Spring.NET、StructureMap、Windsorなどです。

 ASP.NET MVCは拡張性のあるAPIを公開しているため、開発者はコントローラの決定やインスタンス化に関与でき、これにより依存性の注入/IoCフレームワークはそのプロセスとクリーンに統合できます。DI/IOCフレームワークを使用すれば、DinnersControllerからデフォルトのコンストラクタの削除も可能になります。これにより、DinnerRepositoryとのカップリングを完全に削除します。

 依存性の注入/IOCフレームワークをNerdDinnerアプリケーションでは使用しません。しかし、NerdDinnerのコードベースや機能が増えた場合、今後検討できるものです。

Editアクションの単体テストを作成

 ではDinnersControllerのEdit機能を検証する単体テストをいくつか作ってみましょう。まずEditアクションのHTTP-GET版のテストから開始します。

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

  Dinner dinner = dinnerRepository.GetDinner(id);

  if (!dinner.IsHostedBy(User.Identity.Name))
    return View("InvalidOwner");

  return View(new DinnerFormViewModel(dinner));
}

 有効な夕食会がリクエストされたとき、DinnerFormViewModelオブジェクトにより返されたビューが描画されるのを検証するテストを作成します。

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

  // Arrange
  var controller = CreateDinnersController();

  // Act
  var result = controller.Edit(1) as ViewResult;

  // Assert
  Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

 しかしながら、このテストを実行すると、Dinner.IsHostedByメソッドのチェックを実行するためにEditメソッドがUser.Identity.Nameプロパティにアクセスしたときに、null参照例外が投げられ、これが失敗することに気付きます。

 コントローラの基本クラス上のUserオブジェクトは、そのログイン・ユーザーの詳細をカプセル化しており、実行時にコントローラが作成されたとき、ASP.NET MVCによってひも付けられます。DinnersControllerをWebサーバ環境外でテストしているために、そのUserオブジェクトは設定されていません(このためnull参照例外が発生)。

User.Identity.Nameプロパティのモッキング

 モッキング・フレームワークは、テストをサポートする依存オブジェクトの偽バージョンを動的に作成することでテストを簡単にします。例えば、Editアクションのテストでモッキング・フレームワークを使用して、DinnersControllerがテスト用のユーザー名を探すのに使用するUserオブジェクトを動的に作成できます。これにより、テストを実行したときに、null参照がスローされるのを回避できます。

 ASP.NET MVCと使用可能な.NETのモッキング・フレームワークはたくさんあります(それらの一覧はここで見られます)。NerdDinnerアプリケーションのテストには、“Moq“と呼ばれるオープンソースのモッキング・フレームワークを使用します。これはhttp://www.mockframeworks.com/moqから無償でダウンロードできます。

 ダウンロードが終わると、NerdDinner.TestsプロジェクトにMoq.dllアセンブリへの参照を追加します。


図12

 その後、パラメータとしてユーザー名を受け取り、それによりDinnersControllerインスタンス上でUser.Identity.Nameプロパティを“モック”する“CreateDinnersControllerAs(username)”ヘルパー・メソッドをテスト・クラスに追加します。

DinnersController CreateDinnersControllerAs(string userName) {

  var mock = new Mock<ControllerContext>();
  mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
  mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

  var controller = CreateDinnersController();
  controller.ControllerContext = mock.Object;

  return controller;
}

 上記ではMoqを使用して、ControllerContextオブジェクト(User、Request、Response、Sessionなどの実行時のオブジェクトを公開するためのコントローラ・クラスへASP.NET MVCが引き渡すものです)を偽造するMockオブジェクトを作成しています。ControllerContext上のHttpContext.User.Identity.Nameプロパティが、ヘルパー・メソッドへ引き渡したユーザー名の文字列を返すべきであることを示すために、Mock上で“SetupGet”メソッドを呼び出します。

 ControllerContextのプロパティやメソッドはいくつでもモックを作成できます。これを示すために、Request.IsAuthenticatedプロパティに対してSetupGetメッソッドへの呼び出しも追加しています(実際、以下のテストでは必要ありませんが、どのようにRequestプロパティのモックを作成するのかを示すのに役立ちます)。完成したら、ヘルパー・メソッドが返すDinnersControllerへControllerContextモックのインスタンスを割り当てます。

 これで、異なるユーザーが関与しているEditのシナリオをテストするために、このヘルパー・メソッドを使用する単体テストを書くことができます。

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

  // Arrange
  var controller = CreateDinnersControllerAs("SomeUser");

  // Act
  var result = controller.Edit(1) as ViewResult;

  // Assert
  Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

  // Arrange
  var controller = CreateDinnersControllerAs("NotOwnerUser");

  // Act
  var result = controller.Edit(1) as ViewResult;

  // Assert
  Assert.AreEqual(result.ViewName, "InvalidOwner");
}

 いまテストを実行すると、成功します。


図13

UpdateModelヘルパー・メソッドのシナリオのテスト

 EditアクションのHTTP-GET版をカバーするテストを作成しました。ではEditアクションのHTTP-POST版を検証するテストをいくつか作成しましょう。

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

  Dinner dinner = dinnerRepository.GetDinner(id);

  if (!dinner.IsHostedBy(User.Identity.Name))
    return View("InvalidOwner");

  try {
    UpdateModel(dinner);

    dinnerRepository.Save();

    return RedirectToAction("Details", new { id=dinner.DinnerID });
  }
  catch {
    ModelState.AddModelErrors(dinner.GetRuleViolations());

    return View(new DinnerFormViewModel(dinner));
  }
}

 このアクション・メソッドでサポートされている興味深い新しいテストのシナリオは、Controller基本クラス上のUpdateModelヘルパー・メソッドの使用方法です。フォームで入力された値をDinnerオブジェクトのインスタンスにバインドするために、このヘルパー・メソッドを使用します。

 以下は、UpdateModelヘルパー・メソッドが使用できるように、フォーム送信された値をどのように渡しているかを示した2つのテストです。FormCollectionオブジェクトを作成し、ひも付けることで、これを行います。その後、それをコントローラ上の“ValueProvider”プロパティに割り当てます。

 最初のテストは、保存が成功するとブラウザが詳細アクションへリダイレクトされることを検証します。2つ目のテストは、無効な入力が送信されると、そのアクションが編集ビューをエラー・メッセージとともに再表示することを検証します。

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

  // Arrange
  var controller = CreateDinnersControllerAs("SomeUser");

  var formValues = new FormCollection() {
    { "Title", "Another value" },
    { "Description", "Another description" }
  };

  controller.ValueProvider = formValues.ToValueProvider();

  // Act
  var result = controller.Edit(1, formValues) as RedirectToRouteResult;

  // Assert
  Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

  // Arrange
  var controller = CreateDinnersControllerAs("SomeUser");

  var formValues = new FormCollection() {
    { "EventDate", "Bogus date value!!!"}
  };

  controller.ValueProvider = formValues.ToValueProvider();

  // Act
  var result = controller.Edit(1, formValues) as ViewResult;

  // Assert
  Assert.IsNotNull(result, "Expected redisplay of view");
  Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

テストのまとめ

 コントローラ・クラスの単体テストに関連する中心的概念をカバーしました。これらの方法を使って簡単に何百ものアプリケーションの動作を検証する単純なテストを作成できます。

 コントローラやモデルのテストでは、実際のデータベースを必要としないので、非常に速く簡単に実行できます。何秒かで何百もの自動化されたテストを実行でき、加えた変更が何かを壊していないか、すぐにフィードバックが得られます。これは、アプリケーションを継続的に改善、リファクタリング、洗練する自信を私たちにもたらします。

 本章で最後のトピックとしてテストをカバーしましたが、これはテストが開発プロセスの最後に行うものだからではありません! 逆に、開発プロセスでできるだけ早く自動化されたテストを書くべきです。そうすることで、開発時にすぐにフィードバックが得られ、アプリケーションのユース・ケースのシナリオをより考えられるようになり、クリーンな階層化や依存関係が考慮されたアプリケーション設計へあなたを導きます。

 この書籍の後の章では、テスト駆動開発(Test Driven Development、TDD)やASP.NET MVCでそれを使用する方法について議論しています。TDDは、結果が正しいかどうかテストするコードを最初に書く対話的なコーディング手法です。TDDでは実装しようとしている機能を検証するテストを作成することで、各機能を作り始めます。単体テストを最初に書くと、その機能と、それがどのように動作すべきかについて、はっきりと理解できます。テストが書かれた(そして、それが失敗のを検証した)後でのみ、そのテストが検証した実際の機能を実装します。その機能が動作すべきユース・ケースについて、すでに時間を使って考えているので、そこで要求されているものをより理解でき、最善な実装ができます。実装が終わると、テストを再起動すれば、すぐにその機能が正常に動作するかどうかのフィードバックが得られます。10章でTDDについて、さらにカバーします。

次のステップ

 最後にいくつかのまとめやコメントです。

[注:NerdDinnerアプリケーションの完成版はhttp://nerddinner.codeplex.com/からダウンロードできます。] End of Article

 
インデックス・ページヘ  「NerdDinnerチュートリアル」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間