次の方法で共有


自動化された単体テストを有効にする

提供元: Microsoft

PDF のダウンロード

これは、ASP.NET MVC 1 を使用して小規模で完全な Web アプリケーションをビルドする方法を説明する無料の "NerdDinner" アプリケーション チュートリアルの手順 12 です。

手順 12 では、NerdDinner の機能を検証する一連の自動単体テストを開発する方法を示します。それにより、将来アプリケーションに変更や改善を加える際の自信を得られるでしょう。

ASP.NET MVC 3 を使用している場合は、MVC 3 の概要または MVC Music Store に関するチュートリアルに従うことをお勧めします。

NerdDinner 手順 12: 単体テスト

NerdDinner の機能を検証する一連の自動単体テストを開発していきましょう。これにより、将来アプリケーションに変更や改善を加える際の自信を得られます。

単体テストとは

ある朝、仕事に向かっているとき、現在取り組んでいるアプリケーションについての名案を突然ひらめきました。 アプリケーションを劇的に向上させる変更を実装できることに気が付いたのです。 それは、コードをクリーンアップしたり、新しい機能を追加したり、バグを修正したりするリファクタリングである場合があります。

コンピューターに到着すると、「この改善の安全性はどうだろうか?」という疑問に直面します。変更を行うと、副作用が発生したり、何かが壊れたりする場合はどうでしょうか。 単純な変更であれば、実装に数分しかかからないかもしれませんが、すべてのアプリケーション シナリオを手動でテストするのに数時間かかる場合はどうでしょうか。 あるシナリオに対応することを忘れてしまい、壊れたアプリケーションが運用環境に移行された場合はどうでしょうか。 この改善は本当にすべての労力を費やす価値があるでしょうか。

自動単体テストでは、アプリケーションを継続的に強化し、不安なくコーディングを実施できるようにするためのセーフティ ネットを使用できます。 機能を迅速に検証する自動テストを使用すると、自信を持ってコーディングでき、他の方法では快適に実施できない改善を行うことができます。 また、よりメンテナンスしやすく有効期間が長いソリューションの作成にも役立ち、投資収益率の大幅な向上にもつながります。

ASP.NET MVC Framework を使用すると、単体テスト アプリケーションの機能を当然のごとく簡単に使用することができます。 また、テスト優先ベースの開発を可能にするテスト駆動開発 (TDD) ワークフローも使用できます。

NerdDinner.Tests プロジェクト

このチュートリアルの冒頭で NerdDinner アプリケーションを作成したとき、アプリケーション プロジェクトと一緒に進める単体テスト プロジェクトを作成するかどうかを尋ねるダイアログが表示されました。

Screenshot of the Create Unit Test Project dialog. Yes, create a unit test project is selected.Nerd Dinner dot Tests is written as the Test project name.

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

Screenshot of the Solution Explorer navigation tree. Nerd Dinner dot Tests is selected.

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

Dinner モデル クラスの単体テストを作成する

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

まず、テスト プロジェクト内に "Models" という名前の新しいフォルダーを作成し、そこにモデル関連のテストを配置します。 次に、そのフォルダーを右クリックし、[追加]->[新しいテスト] メニュー コマンドを選択します。 これにより、[新しいテストの追加] ダイアログが表示されます。

[単体テスト] を作成し、"DinnerTest.cs" という名前を付けます。

Screenshot of the Add New Test dialog box. Unit Test is highlighted. Dinner Test dot c s is written as the Test Name.

[OK] ボタンをクリックすると、Visual Studio によってDinnerTest.cs ファイルがプロジェクトに追加されます (開きます)。

Screenshot of the Dinner Test dot c s file in Visual Studio.

既定の 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] 属性を持つパブリック メソッドを追加することで、その中でテストを定義できます。

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

[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" テスト パターンを使用してテストを作成していきます。これは "アレンジ、アクト、アサート" を表します。

  • アレンジ: テスト対象のユニットをセットアップする
  • アクト: テスト対象のユニットを実行し、結果をキャプチャする
  • アサート: 動作を検証する

テストを記述するときは、個々のテストの実行が多くなりすぎないようにする必要があります。 それよりも、各テストでは 1 つの概念のみを検証するようにする必要があります (これにより、エラーの原因を特定しやすくなります)。 適切なガイドラインは、各テストに対して 1 つのアサート ステートメントのみを含むようにすることです。 テスト メソッドに複数の アサート ステートメントがある場合は、それらがすべて同じ概念のテストに使用されていることを確認します。 不明な場合は、別のテストを行います。

テストの実行

Visual Studio 2008 Professional (および以降のエディション) には、IDE 内で Visual Studio 単体テスト プロジェクトを実行するために使用できるテスト ランナーが組み込まれています。 [テスト]->[実行]->[All Tests in Solution] (ソリューションのすべてのテスト) メニュー コマンド (または Ctrl + R、A) を選択して、すべての単体テストを実行できます。 または、特定のテスト クラスまたはテスト メソッド内にカーソルを合わせ、[テスト]->[実行]->[Tests in Current Context] (現在のコンテキスト内のテスト) メニュー コマンド (または Ctrl + R、T) を使用して単体テストのサブセットを実行することもできます。

DinnerTest クラス内にカーソルを合わせて、Ctrl + R、T キーを押し、定義した 2 つのテストを実行してみましょう。 この操作を行うと、Visual Studio 内に [テスト結果] ウィンドウが表示され、テストの実行結果を確認できます。

Screenshot of the Test Results window in Visual Studio. The results of the test run are listed within.

注: VS の [テスト結果] ウィンドウには、既定では [クラス名] 列は表示されません。 これを追加するには、[テスト結果] ウィンドウ内を右クリックし、[列の追加と削除] メニュー コマンドを使用します。

2 つのテストの実行に要した時間はほんの 1 秒足らずで、両方とも合格したことがわかります。 特定の規則の検証を行う追加のテストを作成することで、Dinner クラスに追加した 2 つのヘルパー メソッド (IsUserHost() と IsUserRegistered()) に対応するだけでなく、これらを拡張していくことができます。 Dinner クラスに対してこれらすべてのテストを実施することで、今後新しいビジネス規則や検証を追加することがはるかに簡単かつ安全になります。 Dinner に新しい規則のロジックを追加すると、数秒以内に以前のロジック機能が壊れていないことを確認できます。

わかりやすいテスト名を使用すると、各テストが検証している内容をすばやく簡単に理解できることがわかります。 [ツール]->[オプション] メニュー コマンドを使用して、[テスト ツール]->[テストの実行] 構成画面を開き、[Double-clicking a failed or inconclusive unit test result displays the point of failure in the test] (失敗または不確定の単体テストの結果をダブルクリックすると、テストの失敗点が表示されます) チェック ボックスをオンにすることをお勧めします。 これにより、[テスト結果] ウィンドウでエラーをダブルクリックすると、アサート エラーにすぐに移動できます。

DinnersController 単体テストを作成する

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

DinnersController の Details() アクション メソッドを検証する 2 つのテスト メソッドを作成します。 1 つ目は、既存の Dinner が要求されたときにビューを返すことを確認します。 2 つ目は、存在しない Dinner が要求されたときに "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);
    } 
}

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

Screenshot of the code. Both tests have failed.

エラー メッセージを見ると、テストが失敗した理由は、DinnersRepository クラスがデータベースに接続できなかったためだと確認できます。 NerdDinner アプリケーションは、NerdDinner アプリケーション プロジェクトの \App_Data ディレクトリに存在するローカルの SQL Server Express ファイルへの接続文字列を使用しています。 NerdDinner.Tests プロジェクトは、コンパイルされ、別のディレクトリで実行されてから、アプリケーション プロジェクトで実行されるため、接続文字列の相対パスの場所が正しくありません。

これは、SQL Express データベース ファイルをテスト プロジェクトにコピーし、テスト プロジェクトの App.config に適切なテスト接続文字列を追加することで修正 "できます"。 これにより、上記のテストのブロックが解除され、実行されます。

ただし、実際のデータベースを使用した単体テスト コードには、多くの課題が伴います。 具体的には、次のように使用します。

  • それにより、単体テストの実行時間が大幅に遅くなります。 テストの実行にかかる時間が長いほど、頻繁に実行する可能性が低くなります。 単体テストを数秒で、プロジェクトのコンパイルと同じように当然のこととして実行できるようにすることが理想的です。
  • テスト内のセットアップとクリーンアップのロジックが複雑になります。 各単体テストを分離し、(副作用や依存関係がない) 他の単体テストから独立させる必要があります。 実際のデータベースに対して作業するときは、状態に気を配り、テスト間でリセットする必要があります。

このような問題を回避し、テストで実際のデータベースを使用しなくても済む "依存関係の挿入" と呼ばれる設計パターンを見てみましょう。

依存関係の挿入

現在、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 クラスはデータベースにアクセスできる必要があるため、DinnersController クラスが DinnerRepository に対して持つ密接に結合された依存関係により、DinnersController アクション メソッドをテストするためにはデータベースが必要になります。

これを回避するには、"依存関係の挿入" と呼ばれる設計パターンを使用します。これは、依存関係 (データ アクセスを提供するリポジトリ クラスなど) を使用するクラス内でその依存関係を暗黙的に作成しない方法です。 代わりに、コンストラクター引数を使用して依存関係を使用するクラスに依存関係を明示的に渡すことができます。 依存関係がインターフェイスを使用して定義されている場合は、単体テスト シナリオのために "偽の" 依存関係の実装を渡す柔軟性があります。 これにより、データベースへのアクセスを実際に必要としないテスト固有の依存関係の実装を作成できます。

これを実際に見るために、DinnersController を使用して依存関係の挿入を実装してみましょう。

IDinnerRepository インターフェイスを抽出する

最初の手順は、コントローラーが Dinner を取得および更新するために必要なリポジトリ コントラクトをカプセル化する新しい IDinnerRepository インターフェイスを作成することです。

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

または、Visual Studio Professional (およびそれより上位のエディション) に組み込まれているリファクタリング ツールを使用して、既存の DinnerRepository クラスから自動的にインターフェイスを抽出して作成することもできます。 VS を使用してこのインターフェイスを抽出するには、DinnerRepository クラスのテキスト エディターにカーソルを合わせ、[リファクター]->[インターフェイスの抽出] メニュー コマンドを右クリックして選択します。

Screenshot that shows Extract Interface selected in the Refactor submenu.

これにより、[インターフェイスの抽出] ダイアログが起動し、作成するインターフェイスの名前の入力を求められます。 既定では IDinnerRepository となっており、インターフェイスに追加する既存の DinnerRepository クラスのすべてのパブリック メソッドが自動的に選択されます。

Screenshot of the Test Results window in Visual Studio.

[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 つのパブリック 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 クラスを引き続き使用してデータ アクセスを実行します。

しかし、"偽の" dinner リポジトリの実装で、パラメーター コンストラクターを使用して渡すように単体テストを更新できるようになりました。 この "偽の" dinner リポジトリは、実際のデータベースへのアクセスを必要とせず、代わりにメモリ内のサンプル データを使用します。

FakeDinnerRepository クラスを作成する

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

まず、NerdDinner.Tests プロジェクト内に "Fakes" ディレクトリを作成し、それに新しい FakeDinnerRepository クラスを追加します (フォルダーを右クリックし[追加]->[新しいクラス] を選択します)。

Screenshot of the Add New Class menu item. Add New Item is highlighted.

FakeDinnerRepository クラスが IDinnerRepository インターフェイスを実装するようにコードを更新します。 その後、それを右クリックし、[Implement interface IDinnerRepository] (インターフェイス IDinnerRepository の実装) コンテキスト メニュー コマンドを選択します。

Screenshot of the Implement interface I Dinner Repository context menu command.

これにより、Visual Studio によって、既定の "スタブ アウト" 実装を使用して、IDinnerRepository インターフェイス メンバーがすべて FakeDinnerRepository クラスに自動的に追加されます。

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 単体テストに戻りましょう。 次のコードを使用して、メモリ内のサンプル Dinner データが入力された FakeDinnerRepository を DinnersController に使用するようにテスト メソッドを更新できます。

[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);
    }
}

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

Screenshot of the unit tests, both tests have passed.

何より、1 秒足らずで実行でき、複雑なセットアップ/クリーンアップ ロジックは必要ありません。 実際のデータベースに接続することなく、すべての DinnersController アクション メソッド コード (リスト、ページ、詳細、作成、更新、削除を含む) を単体テストできるようになりました。

サイド トピック: 依存関係の挿入フレームワーク
(上記のように) 手動の依存関係の挿入を実行しても問題ありませんが、アプリケーション内の依存関係とコンポーネントの数が増えるにつれて維持するのが困難になります。 .NET には、依存関係管理の柔軟性をさらに高めるのに役立つ依存関係の挿入フレームワークがいくつか存在します。 これらのフレームワークは、"制御の反転" (IoC) コンテナーとも呼ばれ、実行時に依存関係を指定してオブジェクトに渡すための追加レベルの構成サポートを可能にするメカニズムを提供します (ほとんどの場合、コンストラクターの挿入を使用します)。 .NET で一般的な OSS 依存関係の挿入/IOC フレームワークには、AutoFac、Ninject、Spring.NET、StructureMap、Windsor などがあります。 ASP.NET MVC は、開発者がコントローラーの解決とインスタンス化に参加できる機能拡張 API を公開しており、これにより依存関係の挿入/IoC フレームワークはこのプロセス内でうまく統合されます。 DI/IOC フレームワークを使用すると、DinnersController から既定のコンストラクターを削除することもできます。そうすると、DinnerRepository との結合は完全に削除されます。 NerdDinner アプリケーションで依存関係の挿入/IOC フレームワークを使用することはありません。 しかし、NerdDinner のコードベースと機能が成長したら、将来、検討する可能性があります。

編集アクション単体テストを作成する

DinnersController の編集機能を検証する単体テストをいくつか作成してみましょう。 まず、編集アクションの 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));
}

有効な 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));
}

ただし、テストを実行すると、Edit メソッドが Dinner.IsHostedBy() チェックを実行するために User.Identity.Name プロパティにアクセスするときに null 参照例外がスローされるため、失敗することがわかります。

Controller 基底クラスの User オブジェクトは、ログインしているユーザーに関する詳細をカプセル化し、実行時にコントローラーを作成するときに ASP.NET MVC によって入力されます。 Web サーバー環境の外部で DinnersController をテストしているため、User オブジェクトは設定しません (したがって、null 参照例外)。

User.Identity.Name プロパティをモックする

モック フレームワークを使用すると、テストをサポートする偽バージョンの依存オブジェクトが動的に作成されるため、テストが容易になります。 たとえば、Edit アクション テストでモック フレームワークを使用すると、シミュレートされたユーザー名を DinnersController が検索するために使用できるユーザー オブジェクトを動的に作成できます。 これにより、テストの実行時に null 参照がスローされるのを回避できます。

ASP.NET MVC で使用できる .NET モック フレームワークは多数あります (一覧については、http://www.mockframeworks.com/ を参照してください)。

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

Screenshot of the Nerd Dinner navigation tree. Moq is highlighted.

次に、パラメーターとしてユーザー名を取得する "CreateDinnersControllerAs(username)" ヘルパー メソッドをテスト クラスに追加します。その後、DinnersController インスタンスの User.Identity.Name プロパティを "モック" します。

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 オブジェクトを偽装する Mock オブジェクトを作成しています (これは、ASP.NET MVC により Controller クラスに渡され、User、Request、Response、Session などのランタイム オブジェクトを公開します)。 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");
}

これらのテストを実行すると、すべて合格します。

Screenshot of the unit tests that use helper method. The tests have passed.

UpdateModel() シナリオをテストする

Edit アクションの HTTP-GET バージョンに対応するテストを作成しました。 次に、HTTP-POST バージョンの Edit アクションを確認するテストをいくつか作成してみましょう。

//
// 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");
}

テストのまとめ

単体テスト コントローラー クラスに関連する主要な概念について説明しました。 これらの手法を使用すると、アプリケーションの動作を検証する数百のシンプルなテストを簡単に作成することができます。

コントローラーとモデルのテストは実際のデータベースを必要としないため、非常に高速で簡単に実行できます。 数百の自動テストを数秒で実行し、加えた変更によって壊れた箇所がないかどうかについてのフィードバックをすぐに受け取ることができます。 これにより、アプリケーションの継続的な改善、リファクタリング、調整を行う自信を得ることができます。

このチャプターの最後のトピックとしてテストについて説明しましたが、それは、テストを開発プロセスの最後に行うべきものだからというわけではありません。 それどころか、開発プロセスでは、できるだけ早く自動テストを記述する必要があります。 そうすることで、開発時にすぐにフィードバックを得ることができ、アプリケーションのユース ケース シナリオについて慎重に考えるのに役立ち、整理されたレイヤーと結合を念頭に置いてアプリケーションを設計するよう導いてくれます。

この本の後半のチャプターでは、テスト駆動開発 (TDD) と、それを ASP.NET MVC で使用する方法について説明します。 TDD は、結果コードが満たすテストを最初に記述する反復的なコーディング手法です。 TDD では、実装しようとしている機能を検証するテストを作成して、各機能を開始します。 最初に単体テストを記述することで、機能とその動作を明確に理解することができます。 テストを記述した後 (そして、失敗を確認した後) のみ、テストで検証する実際の機能を実装します。 すでに時間をかけてこの機能の動作のユース ケースについて考えているため、要件とその実装に最適な方法について理解を深めることができます。 実装が完了したら、テストを再実行し、機能が正しく動作するかどうかについてすぐにフィードバックを得ることができます。 TDD については、チャプター 10 で詳しく説明します。

次の手順

まとめ記事がいくつかあります。