.NET Tools

テスト駆動開発ツール最前線(後編)

− Mockオブジェクトを使ったNUnit 2.2によるテスト −

(株)ピーデー 川俣 晶
2004/11/17
Page1 Page2 Page3

Mockオブジェクトとは何か

 ここからが今回の記事の目玉ともいうべき大物「Mockオブジェクト」である。Mockオブジェクトは、単体テストで使用されるテクニックの1つである。これは大変に有用なものなので、覚えておく価値がある。

 例えば、システムに1つしかない資源にアクセスするメソッドをテストすることは悩ましい問題を引き起こす。一例を挙げると、データベースに特定のレコードを書き込むメソッドをテストする場合、そのままテストを行うとテスト用のレコードが書き込まれてしまう。しかし、誰かがテストを行うごとにテスト用のレコードが書き込まれることは好ましいことではないし、さらに複数のプログラマが同時にテストを行うと、同じレコードがデータベース上で競合してしまうかもしれない。システムにとって重要なファイルを扱うメソッドでも同じようなことは起こり得るし、レジストリなどに対しても同じことが起こり得る。

通常のテスト
 
不都合が起きるテスト
データベースに特定のレコードを書き込むメソッドをテストする場合、そのままテストを行うとテストのレコードが書き込まれてしまう。

 このような問題を解消するためのテクニックがMockオブジェクトである(Mockは「模造品」という意味)。Mockオブジェクトとは、実際の処理を行わずに定数値だけを返すオブジェクトだ。

 例えば、データベースに一連のデータを書き込むメソッド、Writeメソッドがあるとしよう。このWriteメソッドに、通常はデータベースにアクセスするためのオブジェクトを引数として渡すとする。しかし、テストの際には、データベースにアクセスするためのオブジェクトではなく、データベースにアクセスせずに定数値を返すダミーのオブジェクトを渡すようにする。もちろん、返される定数値は、正常にデータベースの書き込みが完了した場合と同じ値を使う。これにより、Writeメソッドのテストを行うことができるが、データベースに値は書き込まれない。

Mockオブジェクトを使用したテスト
Mockオブジェクトのメソッドは、データベースにアクセスせずに定数値を返す。

 しかし、NUnitに付け加えられたMockオブジェクト生成機能は、このような図式で理解するよりも、あるメソッドを呼ぶ側(表側)と呼ばれる側(裏側)の両面からテストできる、と思った方が分かりやすいかもしれない。

Mockオブジェクトによる両面からのテスト
Mockオブジェクトを利用した場合、対象メソッドの呼び出しの結果だけでなく、対象メソッドがデータベースに対して行うはずの操作も、Mockオブジェクトを通じてチェックすることができる。

 これが意味するところは、従来はテスト対象のメソッドの表側しかチェックしていなかったものが、メソッドの裏側からもチェック可能になることを意味する。表側のチェックは、メソッドがすべての処理を終了してから結果をチェックすることになるので、メソッドのどこで問題が起きたか分かりにくい。

 それに対して、裏側のチェックは、パラメータに渡したMockオブジェクトのメソッドが呼び出されるときに行われるので、テスト対象メソッド実行の途中で意図した呼び出しシーケンスから外れた瞬間にそれを検出することができる。それにより、問題発生個所の検出精度がアップするとともに、より具体的に問題の内容を把握できる可能性が出てくる。

 では、具体的に、このようなMockオブジェクトをどのように用意すればよいのだろうか。もし、あまりに膨大な手間がかかるのであれば、いくらメリットがあろうと現場では実践できないことになる。果たして、どの程度の手間で実現できるのだろうか。次にNUnitの動的Mockオブジェクト生成機能を実際に記述した例を見てみよう。

NUnitのMockオブジェクト

 NUnitには新たにMockオブジェクトのサポートが追加されている。これは、NUnitに内蔵された軽量のもので、インターフェイスまたはMBR(Marshal-By-Reference)クラスからのMockオブジェクトの動的生成、例外の設定、戻り値の設定、期待されたアクションが行われたかどうかの検証の機能を含む。本格的なMockオブジェクトの利用には適さないが、NUnit標準内蔵なので、手軽に安心してMockオブジェクトに取り組むことができる。

 さて、実際にMockオブジェクトの動的生成の機能を使用したサンプル・コードを作成してみた。これを見ながら、具体的にどのようにMockオブジェクトを使用したテストが実現されるかを見てみよう。

 このサンプル・コードは、VS.NETでVB.NETのクラス・ライブラリのプロジェクトを作成した後、nunit.framework.dllとnunit.mocks.dllへの参照を追加し、以下のコードを入力することでテスト可能となる。

Imports NUnit.Framework
Imports NUnit.Mocks

Namespace TestTarget1
  Public Interface ISystemWriter
    Function IsReady() As Boolean
    Sub Write(ByVal name As String, ByVal val As String)
    Sub Close()
  End Interface

  ' テスト対象となるクラス
  Public Class SystemHandler1
    ' Writeメソッドによりログを出力すると想定
    Public Sub Write(ByVal writer As ISystemWriter, ByVal count As Integer)
      If Not writer.IsReady() Then Exit Sub
      For i As Integer = 1 To count
        writer.Write("count", i.ToString())
      Next
      writer.Close()
    End Sub
  End Class
End Namespace

Namespace Test1
  ' テストのためのクラス
  <TestFixture()> _
  Public Class TestSystemHandler1
    Private handler As TestTarget1.SystemHandler1
    Private mockWriter As DynamicMock

    <SetUp()> _
    Public Sub SetUp()
      handler = New TestTarget1.SystemHandler1
      mockWriter = New DynamicMock("MockWriter", GetType(TestTarget1.ISystemWriter))
      mockWriter.ExpectAndReturn("IsReady", True)
      mockWriter.Expect("Write", "count", "1")
      mockWriter.Expect("Write", "count", "2")
      mockWriter.Expect("Write", "count", "3")
      mockWriter.Expect("Close")
    End Sub

    <Test()> _
    Public Sub SimpleMockTest()
      handler.Write(mockWriter.MockInstance, 3)
      mockWriter.Verify()
    End Sub
  End Class
End Namespace
NUnitのMockオブジェクトを利用したVB.NETのサンプル・コード

 さて、このソースには1つのインターフェイスと2つのクラスがあり、名前空間で分類してある。名前空間TestTarget1は、テスト対象となるSystemHandler1クラス。名前空間Test1は、単体テストを記述したクラスを含む名前空間である。ここで意図していることは、SystemHandler1クラスのWriteメソッドのテストである。これは、ファイルに簡単なログを記録する非常に単純化したシチュエーションを想定している(その内容に意味はない)。

 テスト対象のSystemHandler1.Writeメソッドを見てみよう。このメソッドは、引数にISystemWriterインターフェイスを持つオブジェクトを受け取る。これを経由して、ログ・ファイルへの書き込みを行うわけである。そして、もう1つの引数に書き込む項目数を受け取っている。

 内部の動作は3段階になる。まず、IsReadyメソッドを呼び出して、書き込み準備ができているか調べる。準備できていないときには何もせずに終了する。次に、ISystemWriter.Writeメソッドを呼び出して、書き込み動作を行う。この際、引数nameに「count」という文字列を渡し、引数valに1から昇順の整数値を渡す。最後に、Closeメソッドを呼び出して終わる。

 さて、このメソッドをテストするにはどうすればよいのだろうか。単純に考えれば、ISystemWriterインターフェイスを実装したクラスを書いて、そのオブジェクトをSystemHandler1.Writeメソッドの引数に渡せばよい。しかし、いちいちクラスを宣言するのではなく、ここではNUnitの動的なMockオブジェクト生成機能を使って、もっと手軽かつ綿密なテストを記述している。それが、TestSystemHandler1クラスである。

 このTestSystemHandler1クラスは、テストの準備を行うSetUp属性付きのSetUpメソッドと、テストを行うTest属性付きのSimpleMockTestメソッドから構成されている。DynamicMockクラスは動的に生成されるMockオブジェクトの機能を提供するNUnit側のクラスである。メンバ変数のmockWriterがこのクラスのインスタンスを保持している。そして、SimpleMockTestメソッドのmockWriter.MockInstanceという部分により、Mockオブジェクトのインスタンス本体が取得される(Option Strict OnまたはC#で記述する場合などは、ここにキャストが必要とされるかもしれない)。このインスタンス本体をWriteメソッドの引数に渡して、テスト対象となるWriteメソッドを実行している。

Private mockWriter As DynamicMock

<Test()> _
Public Sub SimpleMockTest()
  handler.Write(mockWriter.MockInstance, 3)
  mockWriter.Verify()
End Sub

 さて、具体的にMockオブジェクトはどのように生成されているのだろうか。それはSetUpメソッドの内容を見ると分かる。DynamicMockクラスのインスタンスをNewで生成するときに、ISystemWriterインターフェイスの型情報を引数で指定している。これにより、このインターフェイスを持つオブジェクトが内部的に生成される。そのようなクラスはソース・コード上には宣言されていないが、生成されるのである。

<SetUp()> _
Public Sub SetUp()
  handler = New TestTarget1.SystemHandler1
  mockWriter = New DynamicMock("MockWriter", GetType(TestTarget1.ISystemWriter))
  mockWriter.ExpectAndReturn("IsReady", True)
  mockWriter.Expect("Write", "count", "1")
  mockWriter.Expect("Write", "count", "2")
  mockWriter.Expect("Write", "count", "3")
  mockWriter.Expect("Close")
End Sub

 次に、ExpectAndReturnメソッドとExpectメソッドの呼び出しが5個続いている。ここが今回最大のポイントである。

 まず、ExpectAndReturnメソッドは、指定されたメソッドの呼び出しが行われ、指定された値が戻り値として返ることが期待されていることを示す。第1引数の「IsReady」は期待されるメソッド名、第2引数が戻り値である。つまり、このインスタンスはIsReadyメソッドが呼び出されることがあらかじめ示されていることになる。実際にIsReadyメソッドが呼び出されるのは、SystemHandler1.Writeメソッドが呼び出された後になるので、その前のタイミングで、先に指定を行っているということである。もちろん、ここで行われた指定は、実行される際に実際の呼び出しと付き合わされチェックされる。第2引数のTrueは期待される戻り値である。これは、そのような戻り値が実際に生成されるかチェックするためのものではなく、IsReadyメソッドの呼び出しが発生したとき、Trueを返すために指定された値である。

 ExpectメソッドもExpectAndReturnメソッドと同様であるが、戻り値が存在しない点で異なる。また、ここでは期待される引数も指定されている点に注目してみよう。例えば、mockWriter.Expect("Write", "count", "1")とは、Writeメソッドの第1引数に「count」が、第2引数に「1」が指定されて呼び出されることが指定されている。後で実際に実行される際に、本当にこの値を引数に取って呼び出しが行われるかがチェックされる。

 ここでもう1つ重要なポイントを説明しておこう。ExpectAndReturnメソッドやExpectメソッドは、呼び出される順番が意味を持つ。ここでは、IsReady、Write(3回)、Closeというメソッド名を指定して呼び出しを行っているが、まさにこれらのメソッドはこの順番に、その回数で呼び出されなければならない、という意図を示している。

 例えば、mockWriter.Expect("Write", "count", "1")と、mockWriter.Expect("Write", "count", "2")の順番を入れ替えると、それでテストはパスしなくなってしまう。あくまで、第2引数(Expectメソッド上では第3引数)が「1」であるWriteメソッド呼び出しは、「2」である呼び出しよりも先に発生するためである。

 最後に、SimpleMockTestメソッドの最後に書かれたmockWriter.Verify()という部分に注目しよう。これは、Mockオブジェクトの締めくくりに呼び出すメソッドである。これを呼び出すことによって、すべての処理が終わったことをMockオブジェクトに伝えることができる。その結果、MockオブジェクトはExpectメソッドなどによって用意された呼び出し手順がすべて完了しているか確認することができる。このサンプル・コードでは、例えばIsReadyメソッドが意図せずにFalseを返して終了した場合、残りのメソッドは呼び出されずに終わる。その事実は、このVerifyメソッドの呼び出し時に検出され、テストの失敗が報告される。

 このように、テスト対象のメソッドが、Mockオブジェクトに対してどのような引数を取って、どのメソッドを、どのような順番で呼び出し、戻り値が何であるかをすべて詳しく指定することができる。それによって、テスト対象のメソッドが、完全に意図したとおりに引数に渡したオブジェクトを呼び出しているかどうかを判定できるのである。メソッド終了後に結果をチェックするだけのテストより、より詳細な挙動を確認できる。一方、それだけの詳細なテストでありながら、実際に記述すべきコードはさほど多くない。テスト1件ごとにクラス宣言を書く必要もない。これなら実際に活用できるシーンは多いだろう。


 INDEX
  [.NET Tools]
  テスト駆動開発ツール最前線(前編)
     1.プログラム開発の効率をアップするための方法
     2.GUIテストランナーとテストのための新機能(1)
     3.GUIテストランナーとテストのための新機能(2)
  テスト駆動開発ツール最前線(後編)
     1.Test Driven .NETによる統合されたテスト
   2.Mockオブジェクトとは何か− NUnitのMockオブジェクト
     3.より高度なMockオブジェクトを実現するNMock

インデックス・ページヘ  「.NET Tools」


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

本日 月間