連載:ASP.NET MVC入門【バージョン3対応】

最終回 テスト自動化でアプリケーションの品質向上

山田 祥寛(http://www.wings.msn.to/
2011/11/24
Page1 Page2 Page3 Page4

モック・ライブラリMoqの利用方法

 単体テストの基本を理解したところで、単体テストの実施には欠かせないモック・ライブラリ「Moq」について解説する。

 モックとは、要はテストのためのダミーのオブジェクトのこと。例えば、テスト対象のクラスがHttpContextBaseクラス(HTTP情報)やHttpRequestBaseクラス(リクエスト情報)に依存している場合にも、モックにこれらのクラスの身代わりをさせることで、アプリケーションを実サーバに配置せずともテストを実施できる。また、その時点で未実装のクラスがあった場合にも、モックを利用すれば、あたかも実装済みであるかのようにテストを継続できるようになる。


図5 モックとは?

 Moqは、こうしたモックを簡単に定義できるオープンソースのライブラリの1つである。このMoqの特長は何といっても強い型付けが利用できる点だ。型付けを行うことで、モックのメソッドを定義するにもVisual StudioのIntelliSense機能を利用できるし、戻り値をキャストする必要がない。

 NuGetにも対応しており、[Package Manager Console]ウィンドウから、以下のコマンドを実行するのみでインストールできる。

PM> Install-Package Moq
リスト3 NuGetでのモック・ライブラリMoqのインストール

■Moqを利用してみよう

 それではさっそく、Moqを利用したテスト・メソッドを見ていこう。まずは、モック・プログラミングのシンプルなサンプルとして、未実装のアクション・メソッドをモックに身代わりさせる例からだ。

 あらかじめ、以下のようなHello#Sampleアクションを準備しておくものとする。

public virtual ActionResult Sample()
{
  throw new NotImplementedException();
}
Public Overridable Function Sample() As ActionResult
  Throw New NotImplementedException()
End Function
リスト4 未実装のSampleアクション・メソッド(上:HelloController.cs、下:HelloController.vb)

 中身はNotImplementedException(未実装)例外を返すだけの空のアクションである。ただし、モックとして肩代わりさせる対象のメソッドではvirtual/Overridable(=オーバライド可能)キーワードを付与しておかなければならない点に注意されたい。さもないと、モックを定義する際にInvalidOperationException例外を発生するためだ。

 上のようなSampleアクションをテストするには、以下のようなコードを用意すればよい。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcTemplate.Controllers;
using System.Web.Mvc;
using Moq;

……中略……

[TestMethod]
public void Sample()
{
  // HelloControllerクラスのモックを作成
  var mock = new Mock<HelloController>();

  // Sampleメソッドを準備& 戻り値の登録
  mock.Setup(c => c.Sample()).Returns(
      new ViewResult() { ViewName = "Sample" });

  // モック・オブジェクトを取得&アクションの実行
  var con = mock.Object;
  var result = con.Sample() as ViewResult;

  // 結果の検証
  Assert.AreEqual("Sample", result.ViewName);
}
Imports MvcTemplateVb.MvcTemplateVb
Imports System.Web.Mvc
Imports Moq

……中略……

<TestMethod()>
Public Sub Sample()

  ' HelloControllerクラスのモックを作成
  Dim mock As New Mock(Of HelloController)()

  ' Sampleメソッドを準備& 戻り値の登録
  mock.Setup(Function(c) c.Sample()).Returns( _
    New ViewResult() With {.ViewName = "Sample"})

  ' モック・オブジェクトを取得&アクションの実行
  Dim con = mock.Object
  Dim result = DirectCast(con.Sample(), ViewResult)

  ' 結果の検証
  Assert.AreEqual("Sample", result.ViewName)
End Sub
リスト5 Moqライブラリを利用したSampleアクションのテスト(上:HelloControllerTest.cs、下:HelloControllerTest.vb)

 Moqによるモック定義の手順は、以下のとおり。

Mockクラスの型パラメータとしてモック対象のクラスをひも付け
Setupメソッドで実装するメソッドを準備
Returnsメソッドで期待される戻り値を登録

 以下に、それぞれの構文もまとめておく(いずれもC#の場合)。

new Mock<T>  …… コンストラクタ
Setup(obj => obj.method(args,……))  …… メソッドの準備
Returns(value)  …… 戻り値
Mockライブラリの構文
T:モック対象のクラス。
obj:モック対象のオブジェクト。
method:メソッド名。
args:仮引数。
return:期待される戻り値。

 サンプルでは、以下のような振る舞いをモックとして定義している。

  • Sampleメソッドが、戻り値としてViewResultオブジェクトを返す
  • ビュー名がSampleであること

 複数のメソッドを定義したい場合は、必要な数だけSetup/Returnsメソッドを呼び出せばよい。同じメソッドに対して、引数/戻り値が異なるケースを追加することも可能だ。

 定義したモック・オブジェクトはObjectプロパティで取り出せる。モック・オブジェクトは、そのままモック対象のオブジェクトであるかのように振る舞うことができるので、ここではSampleメソッドを呼び出し、その結果をAssertクラスで検証しているわけだ。

 先ほどと同じくテストを実施してみると、テストの成功が確認できるはずだ。このように、モックを利用することで、本来のメソッドが未実装であるにも関わらず、テストをパスさせることが可能になる。

■Moqを利用したテストの例

 さて、コントローラ・クラスそのものをモックとするのはあまり実践的とはいえないので、以下では、Moqを利用した実用的なテストの例を示す。

(1)ビュー・ヘルパーImageTagのテスト

 ImageTagヘルパーは、第6回のリスト16で作成した自作のビュー・ヘルパーだ。これをテストするコードは、以下のとおり。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Web.Mvc;
using MvcTemplate.Helpers;
using System.Web;
using System.Web.Routing;
using System.IO;

……中略……

[TestMethod]
public void ImageTagTest()
{
  // HtmlHelperオブジェクトを取得&ImageTagメソッドの実行
  var html = this.GetHelper();
  var result = html.ImageTag(
               "http://www.wings.msn.to/image/wings.jpg",
               "サーバーサイド技術の学び舎",
               new { height = 50, width = 150 });

  // 結果の検証
  Assert.AreEqual("<img alt=\"サーバーサイド技術の学び舎\" height=\"50\" src=\"http://www.wings.msn.to/image/wings.jpg\" width=\"150\" />", result.ToHtmlString());
}

private HtmlHelper GetHelper()
{
  // ヘルパー呼び出しのためのHtmlHelperオブジェクトを生成
  var html = new HtmlHelper(
    new ViewContext(
      new ControllerContext(
        (new Mock<HttpContextBase>()).Object,
        new RouteData(),
        (new Mock<ControllerBase>()).Object
      ),
      (new Mock<IView>()).Object,
      new ViewDataDictionary(),
      new TempDataDictionary(),
      new StringWriter()
    ),
    (new Mock<IViewDataContainer>()).Object);

  return html;
}
Imports System.Web.Mvc
Imports Moq
Imports MvcTemplateVb.Helpers
Imports System.Web
Imports System.Web.Routing
Imports System.IO

……中略……

<TestMethod()>
Public Sub ImageTagTest()

  ' HtmlHelperオブジェクトを取得&ImageTagメソッドの実行
  Dim html = Me.GetHelper()
  Dim result = html.ImageTag("http://www.wings.msn.to/image/wings.jpg", "サーバーサイド技術の学び舎", New With {.height = 50, .width = 150})

  ' 結果の検証
  Assert.AreEqual("<img alt=""サーバーサイド技術の学び舎"" height=""50"" src=""http://www.wings.msn.to/image/wings.jpg"" width=""150"" />", result.ToHtmlString())
End Sub

Private Function GetHelper() As HtmlHelper
  ' ヘルパー呼び出しのためのHtmlHelperオブジェクトを生成
  Dim html = New HtmlHelper(
    New ViewContext(
     New ControllerContext(
        (New Mock(Of HttpContextBase)).Object,
        New RouteData(),
        (New Mock(Of ControllerBase)).Object
      ),
      (New Mock(Of IView)).Object,
      New ViewDataDictionary(),
      New TempDataDictionary(),
      New StringWriter()
    ),
    (New Mock(Of IViewDataContainer)).Object
  )

  Return Html
End Function
リスト6 ImageTagヘルパーをテストするコード(上:WingsHelperTest.cs、下:WingsHelperTest.vb)

 複雑にも見えるかもしれないが、ポイントとなるのは太字の部分、HtmlHelperオブジェクトを生成しているプライベート変数GetHelperメソッドだけだ。

 HtmlHelperオブジェクトの生成に必要なHttpContextBase/ControllerBaseなどのオブジェクトをモックで準備している*1。HtmlHelperオブジェクトさえできてしまえば、後は先ほどと同じく、テスト対象のImageTagメソッドを呼び出して、その結果をAssertクラスでチェックすればよい。

*1 ImageTagヘルパーをテストする限りは、ビュー変数や一時変数の情報を必要としないので、「new ViewContext(……)」は「new ViewContext()」とのみ書いても構わない。

■ルーティングのテスト

 続いて、デフォルトで定義されているDefaultルートをテストしてみよう。

 以下は、「~/Books/Show/108」のようなURLがリクエストされた場合に、正しいルートが適用されているか(=期待するルート・パラメータを取得できるか)を確認するテスト・メソッドだ。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Web;
using System.Web.Routing;

……中略……

[TestMethod]
public void DefaultRouteTest()
{
  // ルートの登録
  var routes = new RouteCollection();
  MvcApplication.RegisterRoutes(routes);

  // リクエスト情報を設定
  var context = new Mock<HttpContextBase>();
  context.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath)
      .Returns("~/Books/Show/108");
  context.Setup(c => c.Request.HttpMethod)
      .Returns("GET");

  // ルート・パラメータを取得
  var route = routes.GetRouteData(context.Object);

  // 結果の検証
  Assert.IsNotNull(route);
  Assert.AreEqual("Books", route.Values["controller"]);
  Assert.AreEqual("Show", route.Values["action"]);
  Assert.AreEqual("108", route.Values["id"]);
}
Imports System.Web.Routing
Imports Moq
Imports System.Web

……中略……

<TestMethod()>
Public Sub DefaultRouteTest()

  ' ルートの登録
  Dim routes = New RouteCollection()
  MvcApplication.RegisterRoutes(routes)

  ' リクエスト情報を設定
  Dim context = New Mock(Of HttpContextBase)
  context.Setup(Function(c) c.Request.AppRelativeCurrentExecutionFilePath).
      Returns("~/Books/Show/108")
  context.Setup(Function(c) c.Request.HttpMethod).
      Returns("GET")

  ' ルート・パラメータを取得
  Dim route = routes.GetRouteData(context.Object)

  ' 結果の検証
  Assert.IsNotNull(route)
  Assert.AreEqual("Books", route.Values("controller"))
  Assert.AreEqual("Show", route.Values("action"))
  Assert.AreEqual("108", route.Values("id"))
End Sub
リスト7 Defaultルートをテストするためのコード(上:RouteTest.cs、下:RouteTest.cs)

 ルート・パラメータは、RouteCollectionオブジェクトのGetRouteDataメソッドで取得できる。よって、ここではGetRouteDataメソッドの呼び出しに必要なHttpContextBaseオブジェクトをモックとして準備している。HttpContextBaseオブジェクトには、ルーティングの判定に必要なリクエスト・パスとHTTPメソッドをセットしておこう。

 ルート・パラメータを取得できたら、後はAssertクラスでルート・パラメータが期待どおりの値であるかをチェックするだけだ。IsNotNullメソッドでは、GetRouteDataメソッドの戻り値が空でないか(=ルートにマッチしているか)を確認している。


 INDEX
  ASP.NET MVC入門【バージョン3対応】
  最終回 テスト自動化でアプリケーションの品質向上
    1.単体テストの基本を理解する
  2.モック・ライブラリMoqの利用方法
    3.データベース・アクセスを伴うテスト(1)
    4.データベース・アクセスを伴うテスト(2)
 
インデックス・ページヘ  「ASP.NET MVC入門【バージョン3対応】」


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 記事ランキング

本日 月間