モック・ライブラリMoqの利用方法
単体テストの基本を理解したところで、単体テストの実施には欠かせないモック・ライブラリ「Moq」について解説する。
モックとは、要はテストのためのダミーのオブジェクトのこと。例えば、テスト対象のクラスがHttpContextBaseクラス(HTTP情報)やHttpRequestBaseクラス(リクエスト情報)に依存している場合にも、モックにこれらのクラスの身代わりをさせることで、アプリケーションを実サーバに配置せずともテストを実施できる。また、その時点で未実装のクラスがあった場合にも、モックを利用すれば、あたかも実装済みであるかのようにテストを継続できるようになる。
|
図5 モックとは? |
|
Moqは、こうしたモックを簡単に定義できるオープンソースのライブラリの1つである。このMoqの特長は何といっても強い型付けが利用できる点だ。型付けを行うことで、モックのメソッドを定義するにもVisual StudioのIntelliSense機能を利用できるし、戻り値をキャストする必要がない。
NuGetにも対応しており、[Package Manager Console]ウィンドウから、以下のコマンドを実行するのみでインストールできる。
|
リスト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メソッドの戻り値が空でないか(=ルートにマッチしているか)を確認している。