この記事は機械翻訳されたものです。

ASP.NET MVC

テスト駆動の ASP.NET MVC (機械翻訳)

Keith Burnell

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

中核モデル-ビュー-コント ローラー (MVC) パターンの UI の機能の分離を 3 つのコンポーネントです。モデルには、ドメインの動作とデータを表します。ビューは、モデルの表示を管理し、ユーザーとの対話を処理します。コント ローラーは、ビューとモデルの間の相互作用を調整します。この本質的にテスト困難な UI ロジックの分離をビジネス ロジックからアプリケーションの非常にテストの MVC パターンを実装できます。この資料では依存関係の注入と StructureMap の実装の依存関係の注入を処理するようにコードを設計のベスト プラクティスとソリューションを構造化する方法を含む、ASP.NET MVC アプリケーションのテスト容易性を高めるためのテクニックを説明します。

あなたのソリューションを最大テスト容易化構造

どのような方法よりも、すべての開発者が、新しいプロジェクトを開始する位置の我々 の議論を開始する良い: ソリューションを作成します。私の経験の開発大企業を基にした Visual Studio ソリューションをテスト駆動開発 (TDD) を使用して ASP.NET MVC アプリケーション レイアウトのいくつかのベスト プラクティスを説明します。開始するには、ASP.NET MVC プロジェクトを作成するときに、空のプロジェクト テンプレートを使用してお勧めします。他のテンプレートは実験またはコンセプトの証明を作成するために素晴らしいですが、彼らは一般的に邪魔され、実際のエンタープライズ アプリケーションで不要なノイズを多く含みます。

あらゆる種類の複雑なアプリケーションを作成するとき、n 層アプローチを使用する必要があります。ASP.NET MVC アプリケーション開発のための例示するアプローチを使用してお勧めします図 1図 2、次のプロジェクトを含みます。

  • Web プロジェクトのビュー、モデルの表示、スクリプト、CSS など、すべての UI 固有コードを格納します。この層のコント ローラーのみにアクセスできますが、サービス、ドメイン、および共有のプロジェクト。
  • コント ローラー プロジェクトにはで ASP.NET MVC を使用するコント ローラー クラスが含まれています。この層は、サービス、ドメイン、および共有プロジェクトを通信します。
  • サービス プロジェクトには、アプリケーションのビジネス ロジックが含まれます。この層は、DataAccess、ドメイン、および共有プロジェクトを通信します。
  • DataAccess プロジェクトには取得し、アプリケーションのドライブのデータを操作するためのコードが含まれています。この層は、ドメインと共有プロジェクトを通信します。
  • ドメイン プロジェクトは、アプリケーションによって使用されるドメインのオブジェクトを含む、プロジェクトのいずれかとの通信から禁止されます。
  • 共有プロジェクトにはロガー、定数、およびその他の共通のユーティリティ コードなど他の複数の層に利用できる必要のあるコードが含まれています。それはドメイン プロジェクトとのみ通信を許可しています。

Interaction Among Layers図 1 の相互作用の層の間で

Example Solution Structure図 2 の例のソリューションの構造

あなたのコント ローラーを別の Visual Studio プロジェクトに配置することをお勧めします。この実現容易に方法についてを参照してくださいに投稿 bit.ly/K4mF2B。あなたのコント ローラーを別のプロジェクトに配置することは、さらに UI コードからコント ローラーに存在するロジックを分離できます。Web プロジェクトが本当に、UI に関連するコードのみが含まれることになります。

あなたのテスト プロジェクトを配置する場所 、テスト プロジェクトを検索し、どのように、それらの名前は重要であります。複雑な開発しているときは、エンタープライズ レベルのアプリケーション、ソリューションかなり大きいは、ソリューション エクスプ ローラーで特定のクラスまたはコードを検索する困難ことができますがちです。複数のテスト プロジェクト、既存のコード ベースへの追加はソリューション エクスプ ローラーのナビゲーションの複雑さにのみ追加されます。私は、実際のアプリケーション コードから、テスト プロジェクトを物理的に分離するをお勧めします。テスト フォルダーのソリューション レベルのテストのすべてのプロジェクトを配置することをお勧めします。あなたのすべてのテスト プロジェクトとテスト大幅で単一のソリューション フォルダーを検索する既定のソリューション エクスプ ローラー ビューでノイズを低減して、テストを簡単に検索することができます。

次に、テストの種類を区別する必要があります。そう、あなたのソリューションは、さまざまなテストの種類 (ユニット、統合、パフォーマンス、UI との) が含まれます、分離し、各グループに重要なはより多くのテストの種類。この特定のテストの種類を検索しやすくしてだけでなく、また、特定の種類のすべてのテストを簡単に実行できます。最も人気のある Visual Studio 生産性ツール スイート、ReSharper のいずれかを使用している場合 (jetbrains.com/ReSharper) または CodeRush (devexpress.com/CodeRush)、任意のフォルダー、プロジェクト、またはソリューション エクスプ ローラーでクラスを右クリックし、アイテムに含まれるすべてのテストを実行することができますコンテキスト メニューを得る。テストのテストの種類によってグループ化、テスト計画、テスト ソリューション フォルダー内の書き込みの種類ごとのフォルダーを作成します。

図 3 テスト タイプのフォルダーの数を含む例テスト ソリューション] フォルダーが表示されます。

An Example Tests Solution Folder図 3 テスト ソリューション フォルダーの例

テスト プロジェクトの名前を付けるどのようにあなたのテスト プロジェクトを名前を場所として重要であります。のどの部分をアプリケーションの各テスト プロジェクトにテストが簡単に区別することができるようにして、テストのプロジェクト タイプが含まれています。このため、次の規則を使用して、テスト プロジェクトの名前には良いアイデアは: [完全名の [テスト プロジェクト] .test と。 [テスト タイプ]。これは一目では、まさに何層プロジェクトの下でのテストは、どのような種類のテストが実行されているを確認するには、ことができます。あなたは、テスト プロジェクトの種類に固有のフォルダーに入れて考えるかもしれないと、テストの種類、テスト プロジェクトの名前を含む冗長ですが、ソリューション フォルダーはソリューション エクスプ ローラーでのみ使用され、プロジェクト ファイルの名前空間に含まれていない覚えています。だから、コント ローラーの単体テスト プロジェクト、Tests\Unit のソリューション フォルダーにですが、namespace—TestDrivingMVC.Controllers.Test.Unit—doesn't 反映そのフォルダー構造。プロジェクトの名前を付けるときのテストの種類の追加の名前の衝突を避けるために、テストの種類、エディター内で動作しているを確認する必要があります。図 4 ソリューション エクスプ ローラーでテスト プロジェクトを示しています。

Test Projects in Solution Explorer図 4 テスト プロジェクト ソリューション エクスプ ローラーで

あなたのアーキテクチャへの依存関係の注入を導入

あなたが得ることができない非常に遠くのユニットをテスト対象コード内の依存関係が発生する前に、n 層アプリケーションをテストします。これらの依存関係をアプリケーションの他のレイヤーや彼らは完全に外部コードなど、データベース、ファイル システムまたは Web サービス) にことができます。単体テストを記述している場合は、このような状況を正しく処理し、テスト代替 (モック、偽造品またはスタブ) を使用する場合する必要があります、外部の依存関係が発生します。テスト代替の詳細については、「探索の連続体のテスト ダブルスに」を参照してください (msdn.microsoft.com/magazine/cc163358) 2007 年 9 月号の MSDN Magazine で。ただし、ダブルスを提供するテストの柔軟性を活用することができます前に、コードの依存関係の注入を処理するために設計される必要があります。

依存関係の注入依存性の注入はクラスが必要な依存関係を直接インスタンス化するクラスではなく、具体的な実装を注入するプロセス。かかるクラスの実際その依存関係の具体的実装は認識されませんが、依存関係をバックアップのインターフェイスのみを知っている; コンクリートの実装には、かかるクラスまたは、依存関係注入フレームワークのいずれか提供されています。

依存関係の注入の目標は、非常に疎結合のコードを作成することです。疎結合、簡単に代替テスト二重実装の依存関係を単体テストを記述するときできます。

依存関係の注入を達成するために 3 つの主な方法があります。

  • プロパティの挿入
  • コンス トラクターによる挿入
  • 依存関係注入フレームワーク/(ディ/IoC フレームワークとしてこのポイントからとするコントロール コンテナーの反転を使用してください。

プロパティの挿入に示すように設定するには、その依存関係を有効にすると、オブジェクトのパブリック プロパティを公開図 5。このアプローチは簡単で、工具は必要ありません。

図 5 プロパティの挿入

// Employee Service
public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService() {}
  public ILoggingService LoggingService { get; set; }
  public decimal CalculateSalary(long employeeId) {
    EnsureDependenciesSatisfied();
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
  private void EnsureDependenciesSatisfied() {
    if (_loggingService == null)
      throw new InvalidOperationException(
        "Logging Service dependency must be satisfied!");
    }
  }
}
// Employee Controller (Consumer of Employee Service)
public class EmployeeController : Controller {
  public ActionResult DisplaySalary(long id) {
    EmployeeService employeeService = new EmployeeService();
    employeeService.LoggingService = new LoggingService();
    decimal salary = employeeService.CalculateSalary(id);
    return View(salary);
  }
}

このアプローチに 3 つの欠点があります。 まず、担当の依存関係を供給、消費者を置きます。 次に、オブジェクトを使用する前に、依存関係が設定されているようにガード コードを実装する必要があります。 最後に、あなたのオブジェクトが依存関係の数が増えると、オブジェクトをインスタンス化に必要なコードの量も増加します。

コンス トラクターによる挿入を使用して依存関係の注入を実装するコンス トラクターがインスタンス化されるときは、そのコンス トラクター経由でクラスへの依存関係で示すように供給する図 6。 この方法も簡単ですが、プロパティの挿入とは異なり、あなたのクラスの依存関係が常に設定されていることが保証されます。

図 6 コンス トラクターによる挿入

// Employee Service
public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService(ILoggingService loggingService) {
    _loggingService = loggingService;
  }
  public decimal CalculateSalary(long employeeId) {
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
}
// Consumer of Employee Service
public class EmployeeController : Controller {
  public ActionResult DisplaySalary(long employeeId) {
    EmployeeService employeeService =
      new EmployeeService(new LoggingService());
    decimal salary = employeeService.CalculateSalary(employeeId);
    return View(salary);
  }
}

残念ながら、このアプローチは、依存関係を指定するには、消費者が必要です。 また、それだけ小さな本当に適しています。 大規模なアプリケーションは通常、オブジェクトのコンス トラクター経由で供給するあまりにも多くの依存関係があります。

依存関係の注入を実装する 3 番目の方法はディ/IoC フレームワークを使用することです。 ・ ディ ・/IoC フレームワークは完全に消費者からの依存関係を供給する責任を削除し、デザイン時に、依存関係を構成し、実行時に解決することができます。 多くのディ/IoC フレームワークの統一 (マイクロソフト提供)、StructureMap、ウィンザー城、Ninject などの .net のです。 すべて、ディ/IoC フレームワークは基になる概念は同じです、1 つは、通常の選択を個人の好みです。 ・ ディ ・/IoC フレームワークでこの資料を示すため、StructureMap を使用します。

StructureMap の次のレベルへの依存関係の注入を撮影

StructureMap (structuremap。 広く採用された依存関係注入フレームワークは純) です。 それをインストールすると、NuGet どちらかパッケージ マネージャー コンソール (インストール パッケージ StructureMap) または NuGet パッケージ マネージャー GUI を介して (プロジェクトの参照] フォルダーを右クリックし、NuGet パッケージの管理を選択します)。

StructureMap の依存関係を構成する StructureMap の ASP.NET MVC の実装には、まず StructureMap それらを解決する方法を知っているので、依存関係を構成するのには。 この 2 つの方法のいずれかの Global.asax の Application_Start メソッドで行います。

最初の方法は手動で StructureMap 特定抽象の実装のためそれは特定の具象実装を使用するように指示することです。

ObjectFactory.Initialize(register => {
  register.For<ILoggingService>().Use<LoggingService>();
  register.For<IEmployeeService>().Use<EmployeeService>();
});

この方法の欠点は、アプリケーションでは、それぞれの依存関係を手動で登録する必要があるし、大規模なアプリケーションをこの退屈になることです。 また、ASP.NET MVC サイトの Application_Start であなたの依存関係を登録するため、Web レイヤーの依存関係を持つアプリケーションの他のすべてのレイヤーの直接の知識が必要です。

StructureMap の自動登録およびスキャン機能を使用して、アセンブリを検査し、依存関係を自動的に配線することもできます。 このアプローチでは、StructureMap アセンブリをスキャンし、インタ フェースを検出すると、IFoo という名前のインターフェイスが条約マップ具体的実装 Foo によってだろう概念に基づいて、関連付けられたコンクリートの実装を検索します。

ObjectFactory.Initialize(registry => registry.Scan(x => {
  x.AssembliesFromApplicationBaseDirectory();
  x.WithDefaultConventions();
}));

StructureMap 依存関係リゾルバー あなたの依存関係を構成した後、コード ベースからそれらにアクセスすることがでく必要があります。 これは、依存関係の競合回避モジュールを作成し、(は、アプリケーションの依存関係のあるすべて層でアクセスする必要がありますので)、共有プロジェクト内を検索する行います。

public static class Resolver {
  public static T GetConcreteInstanceOf<T>() {
    return ObjectFactory.GetInstance<T>();
  }
}

(Microsoft ASP.NET MVC 3 で説明を持つ DependencyResolver クラスを導入したのでそれを少しコールするよう) 競合回避モジュールのクラスは 1 つの関数が含まれている単純な静的クラスです。 関数がジェネリック パラメーターは、具体的な実装を求めているし、T を返します、渡されたインターフェイスの実際の実装インターフェイスを表す T を受け入れます。

新しいリゾルバ クラスをコードで使用する方法に飛び込む前に、ASP.NET MVC 3 で紹介した、IDependencyResolver インターフェイスを実装するクラスを作成するのではなく私自身の自家製依存関係リゾルバーを書きましたなぜアドレスをしたいです。 IDependencyResolver 機能を含めることは、ASP.NET MVC と適切なソフトウェア プラクティスの促進は素晴らしい大股に畏敬させる追加です。 残念ながら、System.Web.MVC DLL では、存在し、非 Web 層の私のアプリケーション アーキテクチャの Web テクノロジ固有ライブラリへの参照をしたくないです。

コード内の依存関係を解決今ではすべてのハードワークを行って、コード内の依存関係の解決は簡単。 すべてを行う必要のある競合回避モジュールのクラスの静的の GetConcreteInstanceOf 関数を呼び出すし、それに示すように、具体的な実装を追求しているインターフェイスを渡す図 7

図 7 のコード内の依存関係を解決します。

public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService() {
    _loggingService = 
      Resolver.GetConcreteInstanceOf<ILoggingService>();
  }
  public decimal CalculateSalary(long employeeId) {
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
}

単体テストでのテスト代替の注入を StructureMap の利点を取って今では、消費者からの介入なしの依存関係を挿入することができますので、コードを設計の単体テストでの依存関係を正しく扱うには、元のタスクに戻りましょう。 ここでは、シナリオです。

  • タスクは、EmployeeService の CalculateSalary メソッドから返す給料値生成 TDD を使用してロジックを記述することです。 (EmployeeService と CalculateSalary の機能を見つける図 7.)
  • CalculateSalary 関数へのすべての呼び出しのログを記録する必要がありますような要件です。
  • ログ サービスのインターフェイス定義ですが、実装が不完全であります。 現在、ログ サービスを呼び出すと、例外がスローされます。
  • タスクの作業ログ サービスを開始するスケジュールされる前に完了する必要があります。

この種のシナリオの前に発生した可能性が高い以上です。 今、ただし、適切なアーキテクチャ テストを二重を置くことによって、依存関係にネクタイを断つことができる場所であります。 すべてのテスト プロジェクト間で共有できるプロジェクトで私のテスト代替を作成したいと思います。 見ることができます図 8、私は私のテストのソリューション フォルダーに、共有プロジェクトを作成しました。 プロジェクト内の偽物のフォルダーを追加私のテストを完了するは、ILoggingService の偽の実装を必要なため。

Project for Shared Test Code and Fakes
図 8 プロジェクト共有テスト コードと偽物

ログ サービスのための偽の作成は簡単です。 まず、LoggingServiceFake という私の偽物フォルダー内のクラスを作成します。 LoggingServiceFake は、ILoggingService とそのメソッドを実装する必要があることを意味は、EmployeeService を期待して、契約を満たす必要があります。 定義では、偽インターフェイスを満たすのに十分なコードを含む代役です。 通常、つまり void メソッドの空の実装が、ハードコードされた値を返す return ステートメントの関数の実装を含むようになります。

public class LoggingServiceFake : ILoggingService {
  public void LogError(string message, Exception ex) {}
  public void LogDebug(string message) {}
  public bool IsOnline() {
    return true;
  }
}

今では、偽の実装は、私のテストを書くことができます。 開始するには、TestDrivingMVC.Service.Test.Unit ユニットのテスト プロジェクトにテスト クラスを作成して、前述した名前付け規則のあたりに、私はそれ EmployeeServiceTest に示すように名前を付けます図 9

図 9 EmployeeServiceTest テスト クラス

[TestClass]
public class EmployeeServiceTest {
  private ILoggingService _loggingServiceFake;
  private IEmployeeService _employeeService;
  [TestInitialize]
  public void TestSetup() {
    _loggingServiceFake = new LoggingServiceFake();
    ObjectFactory.Initialize(x => 
      x.For<ILoggingService>().Use(_loggingServiceFake));
    _employeeService = new EmployeeService();
  }
  [TestMethod]
  public void CalculateSalary_ShouldReturn_Decimal() {
    // Arrange
    long employeeId = 12345;
    // Act
    var result = 
      _employeeService.CalculateSalary(employeeId);
    // Assert
    result.ShouldBeType<decimal>();
  }
}

ほとんどの部分については、テスト クラスのコードは簡単です。 特に細心の注意を払う行です。

ObjectFactory.Initialize(x =>
    x.For<ILoggingService>().Use(
    _loggingService));

これは StructureMap 指示コードです私たち以前競合回避モジュールのクラスを作成するときに、LoggingServiceFake を使用する ILoggingService の解決を試みます。 テスト クラスですべてのテストを実行する前にこのメソッドを実行するには、ユニット テスト フレームワークを告げる TestInitialize でマークされたメソッドでこのコードを置きます。

ディ ・ IoC と StructureMap ツールの力を使用して、完全に、ログ サービスにネクタイを断つことができるんです。 これを私私のコーディングと単体テスト ログ サービスの状態によって影響されることがなく完了してすべての依存関係に依存しない本当の単体テストをコード化することができます。

StructureMap コント ローラーの既定のファクトリとして使用 ASP.NET MVC コント ローラーのインスタンス化のカスタム実装をアプリケーションに追加することができます、機能拡張ポイントを提供します。 DefaultControllerFactory から継承するクラスを作成する (を参照してください図 10)、コント ローラーの作成方法を制御できます。

図 10 カスタム コント ローラー ファクトリ

public class ControllerFactory : DefaultControllerFactory {
  private const string ControllerNotFound = 
  "The controller for path '{0}' could not be found or it does not implement IController.";
  private const string NotAController = "Type requested is not a controller: {0}";
  private const string UnableToResolveController = 
    "Unable to resolve controller: {0}";
  public ControllerFactory() {
    Container = ObjectFactory.Container;
  }
  public IContainer Container { get; set; }
  protected override IController GetControllerInstance(
    RequestContext context, Type controllerType) {
    IController controller;
    if (controllerType == null)
      throw new HttpException(404, String.Format(ControllerNotFound,
      context.HttpContext.Request.Path));
    if (!typeof (IController).IsAssignableFrom(controllerType))
      throw new ArgumentException(string.Format(NotAController,
      controllerType.Name), "controllerType");
    try {
      controller = Container.GetInstance(controllerType) 
        as IController;
    }
    catch (Exception ex) {
      throw new InvalidOperationException(
      String.Format(UnableToResolveController, 
        controllerType.Name), ex);
    }
    return controller;
  }
}

新しいコント ローラー ファクトリでは、私の StructureMap ObjectFactory に基づいて設定を取得しますパブリック StructureMap コンテナー プロパティがある (構成されているの図 10 、Global.asax で)。

次に、いくつかの型をチェックし、StructureMap コンテナーを使用して、指定したコント ローラーの型パラメーターに基づく現在のコント ローラーを解決するのには GetControllerInstance メソッドのオーバーライドがあります。 StructureMap の自動登録を使用して StructureMap の初期構成時スキャン機能は何もないので行う必要。

カスタム コント ローラー ファクトリを作成する利点は、あなたはもはやあなたのコント ローラーのパラメーターなしのコンス トラクターに限定することです。 この時点で自分が要求される可能性があります、「はコント ローラーのコンス トラクターのパラメーターを供給することについてはどうだろうか」DefaultControllerFactory と StructureMap の機能拡張のおかげで、する必要はありません。 あなたのコント ローラーのパラメーター化されたコンス トラクターを宣言すると、コント ローラーが新しいコント ローラー ファクトリで解決されると、依存関係が自動的に解決されます。

見ることができます図 11、HomeController のコンス トラクターには、IEmployeeService パラメーターを加えました。 コント ローラーが新しいコント ローラー ファクトリで解決されると、コント ローラーのコンス トラクターで必要なパラメーターは自動的に解決されます。 つまり、コント ローラーの依存関係を手動で解決するコードを追加する必要はありません — が、まだ偽物、前述したように使用できます。

図 11、コント ローラーの解決

public class HomeController : Controller {
  private readonly IEmployeeService _employeeService;
  public HomeController(IEmployeeService employeeService) {
    _employeeService = employeeService;
  }
  public ActionResult Index() {
    return View();
  }
  public ActionResult DisplaySalary(long id) {
    decimal salary = _employeeService.CalculateSalary(id);
    return View(salary);
  }
}

これらのプラクティスやテクニックは、ASP.NET MVC アプリケーションでを使用して、あなた自身をより容易に、クリーナーの TDD プロセスの位置を決めます。

Keith Burnell は Skyline Technologies に勤務するシニア ソフトウェア エンジニアです。大規模な ASP.NET および ASP.NET MVC Web サイトの開発を中心として、ソフトウェアの開発に 10 年以上携わっています。Burnell は開発者コミュニティで活発に活動しており、ブログ ( dotnetdevdude.com)、Twitter ( twitter.com/keburnell) から連絡することができます。

この記事のレビュー次の技術的な専門家のおかげで: ジョン Ptacek とクラークの販売