Kent Beck Testing Framework 入門
Kent Beck Testing Framework 入門
はじめに
Smalltalker として有名な Kent Beck は,ソフトウェアテスト用プログラムをフレームワークレベルにまで拡張し,テストを体系的に取り扱う方法を提唱しました.そのフレームワークは Testing Framework と呼ばれ,Smalltalk の他にも Java, C++, Visual Basic 用の Testing Framework が提供されています(それぞれ JUnit, CppUnit, VBUnit と呼ばれています).
同じソフトウェアテストを行う製品としては,Windows の場合 Rational 社(以前は Microsoft 社)の Visual Test が有名です.Visual Test の場合,スクリプト言語でユーザの操作をシミュレートしてテストを行うのに対し, Testing Framework では,開発言語でソフトウェア内部にテストコードを記述するところが異なります.
Testing Framework についての情報は,Armaties Software の eXtreme Programming のページからたどることができます( http://www.armaties.com/extreme.htm ).JUnit, CppUnit, VBUnit などもここの FTP サイトから入手できますので,興味のある方はぜひダウンロードしてください( ftp://www.armaties.com/TestingFramework/ ).
ここでは,Kent Beck の Testing Framework をデザインパターンを使って紹介したいと思います.以下では,Testing Framework を TF と略すことにします.
なおこの記事は上記 FTP サイトから入手したソフトウェアおよびドキュメント,JavaReport のJUnit 解説記事(Eric Gamma and Kent Beck, "JUnit: A Cook's Tour", JavaReport, May, 1999)を参考にしています.
Command パターン
テストコードを書く場合,例えばクラスごとにクラスメソッド test を用意してそこにテストコードを書いている人が多いのではないでしょうか.
しかし TF では,もう一歩踏み込んでテストそのものをオブジェクトとして扱います.テストというのは処理を表しているので,ここで Command パターンを適用すればいいことがわかります.Command パターンにおける Command クラスに相当するのが TestCase クラスです.
上の例は,Sample クラスのテストをするために,TestCase クラスから SampleTest を継承して,run メソッドをオーバーライドしています.TF でまず行うことは, TestCase のサブクラスであるテストクラスを作る ことです(継承が使えない VBUnit のユーザは後述の Strategy パターンのセクションまで待ってください).
Template Method パターン
今度は,テストの手順を見てみましょう.一般に,テストは
- テストの準備 -- setUp
- テストの実行 -- runTest
- あとかたづけ -- tearDown
という手順を踏んでいます.このアルゴリズム自体は変わりません.ですから,ここで Template Method パターンを適用すればいいことがわかります.テスト全体の実行を行う run メソッドは,setUp, runTest, tearDown の 3 つを順に行う Template Method にします.
public void run() { setUp(); runTest(); tearDown(); }
Command パターンのセクションでは,run メソッド全体をオーバーライドするような書き方でしたが,実際にはこの 3 つのメソッドをオーバーライドすれば十分です.したがって, テストクラスを作るときは,
- setUp メソッドにテストの準備
- runTest メソッドにテスト本体
- tearDown メソッドにあとかたづけ
のコードを書く ことになります.
Fixture について
Kent Beck は,テストを行うための部品となるオブジェクトを Fixture と呼んでいます(Fixture は備品とか設備という意味です).一般に Fixture はテスト対象となるクラスのインスタンスで,テストクラスのインスタンス変数として用意します.したがって,
- Fixture オブジェクトをインスタンス変数として定義する
- setUp メソッドで Fixture オブジェクトの初期化
- runTest メソッドで Fixture オブジェクトを使ったテスト
- tearDown メソッドで Fixture オブジェクトのあとかたづけ
というプログラムになります.
Strategy パターン (VBUnit のみ)
Visual Basic では, インターフェイス継承しかサポートされていないため,Template Method パターンではなく Strategy パターンが使われています.次の図にあるように, ITestFixture クラスモジュールが Strategy パターンの Strategy クラスに相当します.
TestCase クラスは,Run メソッド内で ITestFixture に対してSetup, RunTest, TearDown メソッドを順に呼び出します.したがってVBUnit では,ITestFixture をインターフェイス継承したクラスモジュールがあたかもカスタマイズされたテストクラスであるかのように扱います. もちろん 3 つの メソッドの意味はいままでと同じです.したがってテストクラスモジュールのコーディングは以下のようになります.
Implements ITestFixture Private Sub ITestFixture_Setup(TestCaseContainer As VBUnit.TestCase) 'テストの準備(Fixture オブジェクトの初期化) End Sub Private Sub ITestFixture_RunTest() 'テスト本体 End Sub Private Sub ITestFixture_TearDown() 'あとかたづけ(Fixture オブジェクトのあとかたづけ) End Sub
Setup メソッドの引数,TestCaseContainer は テストを実行する TestCase クラスです.通常はこのオブジェクトを保持しておき, assert メソッド等を利用します.まとめると,VBUnit では, ITestFixture をインターフェイス継承してテストクラスを作る ことになります.
ところで,テスト自体は TestCase から実行しないといけないので,利用方法は以下のようになります.
Dim aTestCase As TestCase Set aTestCase = New TestCase Set aTestCase.TestFixture = New SampleTest aTestCase.Run
このように, 利用するときは TestCase クラスの TestFixture プロパティにテストオブジェクトをセットする ことを忘れないようにしましょう.
Composite パターン
以上のようにして作ったテストクラスがたくさんあると仮定しましょう.プログラムの修正を行った後,テストコードは何度も実行されることになります.しかし,テストクラスを一つ一つ取り出して実行するのは手間がかかります.複数のテストと単体のテストを区別なくひとつのテストとして実行できると便利ですね.
そのため TestCase に対して Composite パターンを使います.Composite パターンを使えば,合成されたオブジェクトと単体のオブジェクトを統一的に扱うことが可能になります.
上図にあるように,Composite パターンの Composite クラスに相当するのが TestSuite クラスです.TestCase と TestSuite 共通のインターフェイスを受け持つのが Test クラスになります.最初の Command パターンで TestCase クラスを導入したとき,なぜ名前を Test クラスにしなかったのかはおそらくこの Test インターフェイスを導入したかったためでしょう.
TestSuite クラスの addTest メソッドで テストオブジェクトを追加します.その後,TestSuite クラスの run メソッドを呼び出すと,登録された各 Test オブジェクトに対して run メソッドが順に実行されます.
TestSuite suite = new TestSuite(); suite.addTest(new SampleTestA("SampleTestA")); suite.addTest(new SampleTestB("SampleTestB")); suite.run();
Adapter パターン
Command パターンを使っていてよく問題になるのが,Command クラスの数が膨れ上がることです.前節のコードを見ればわかりますが,Sample クラスのテストを行うとして,このままでは一つのテストコードごとに一つのテストクラスを用意しなくてはなりません.Sample クラスのテストコードが 10 個ある場合,10 個のテストクラスを用意しなければならないのでしょうか? もしそうだとしたらあまりにも手間がかかります.
テストクラスのテストコード用メソッドとして testA, testB という 2 つのメソッドがあるとします.この 2 つのメソッドを runTest メソッドに変換する方法を考えなくてはなりません.インターフェイスの変換と聞いて思い出すのが Adapter パターンですね.
Adapter パターンには,継承を使った class adapter と委譲を使った object adapter の 2 種類があります.例えば,class adapter を使った実装を考えてみると次のようなクラス図になるでしょう.
SampleTestA クラスの runTest メソッドから testA メソッドを呼び,SampleTestB クラスの runTest メソッドから testB メソッドを呼びます(GoF の class adapter の実装と少し違いますが,SampleTest クラスはすでに TestCase のサブクラスであるため縮退した形になっています).でもこれでは,クラスの数が増えるという問題は何も解決していません.
そこで, メソッドをパラメータ化して GoF にある Adapter パターンを修正します.つまり, runTest メソッドから呼ぶメソッドが動的に指定できればよいのです.メソッドをパラメータ化するという話はプログラミング言語依存ですので,ここからは JUnit, VBUnit, CppUnit で分けて説明しましょう.
(注: クラス数が増大する 1 番の問題点は,名前空間が汚染されるということでしょう.Java の場合,無名クラスを使ってもこの問題を解決することができます.C++ の場合は,ネストクラスで解決できます.こちらの方がプログラムとしてわかりやすくなる場合は利用してもよいでしょう.つまり状況に応じて使い分けるのがよいと思います.)
JUnit の場合
JUnit では,TestCase コンストラクタにテストメソッド名を文字列として指定すれば,そのメソッドを runTest メソッドから呼び出すようになっています(Reflection の機能を使用).使い方は以下のようになります.
TestSuite suite = new TestSuite(); suite.addTest(new SampleTest("testA")); suite.addTest(new SampleTest("testB")); suite.run();
このように SampleTestA クラス, SampleTestB クラスを新たに作る必要はありません.
さらに JUnit では Reflection の機能を使い, テストクラスの public メソッドでかつ名前が test で始まるメソッドをすべてテストメソッドとし,各テストメソッド用に対応するテストクラスのインスタンスを集めて TestSuite クラスに登録するという便利な機能があります.この場合は以下のように非常にシンプルな形になります.
TestSuite suite = new TestSuite(SampleTest.class); suite.run();
JUnit を使う場合はこの方法がいちばん簡単です.だから test で始まる名前のテストメソッドを定義し, TestSuite コンストラクタに class を渡す ことになります.runTest メソッドをオーバーライドする必要はありません.
VBUnit の場合
VBUnit(Beta 1) では今のところテストメソッドに対する Adapter パターンはサポートされていません.けれども Visual Basic 6.0 からは CallByName 関数という,あるオブジェクトのメソッドをそのメソッド名の文字列を指定するだけで呼び出すことができる非常に便利なものが導入されました.
今のバージョンは対応していませんが,将来この CallByName 関数を使えば,JUnit にあるようなメソッド名を指定する方法は実装できるでしょう.もちろんそれでも JUnit にあるテストメソッドを一度にすべて TestSuite に登録する方法は実装できないのですが.
今の段階では, VBUnit の場合マニュアルでやる以外方法がありません.つまりテストメソッドをいくつか書いておいて RunTest メソッド内にそれらのテストメソッドを直接書いてしまいます.こうするとみんな一つのテストになってしまうので,それを防ぐために TestCase クラスに Trace メソッドが用意されています.これは,テストの例外が発生したときのカレントのテスト名を指定しておくためのものです.利用する場合は,次のようになります.
Implements ITestFixture Private m_pTestCase As TestCase Private Sub ITestFixture_Setup(TestCaseContainer As VBUnit.TestCase) 'オーナーである TestCase オブジェクトを保持しておく Set m_pTestCase = TestCaseContainer 'Fixture オブジェクトの初期化をここに書く End Sub Private Sub ITestFixture_RunTest() TestA TestB End Sub Public Sub TestA() m_pTestCase.Trace "TestA" 'TestAのテストコードをここに書く End Sub Public Sub TestB() m_pTestCase.Trace "TestB" 'TestBのテストコードをここに書く End Sub
このようにしても他の JUnit や CppUnit と違って Setup と TearDown がその都度実行されてないことに注意しましょう.
CppUnit の場合
C++ の場合,文字列としてではなくメンバ関数ポインタでメソッドをパラメータ化することが可能です.CppUnit の場合は,次の object adapter に近いのですが,テンプレート機能を使っているところが異なります.
上図の TestCaller クラスは,SampleTest クラスのテストメソッドへのポインタを保持しています.そして,SampleTest のオブジェクトを参照し,そのメソッドを runTest メソッドから呼び出します.setUp と tearDown メソッドでは 参照オブジェクトにメソッドを委譲しているだけです.
TestCaller クラスはテンプレートですので,利用する場合は,次のようになります.
TestSuite *suite = new TestSuite; suite->addTest(new TestCaller<SampleTest>("testA", SampleTest::testA)); suite->addTest(new TestCaller<SampleTest>("testB", SampleTest::testB)); suite->run();
TestCaller コンストラクタの第 1 引数は テスト名で,第 2 番目がメソッド関数ポインタです. JUnit のように一度に TestSuite オブジェクトに登録する方法はありません.
suite クラスメソッド
このまでの話で,1 つのテストクラスに対して,複数のテストクラスインスタンスがあり,それぞれがテストクラスの各テストメソッドに対応している,ということがわかりました.
これらのテストクラスインスタンスを集めたものを 1 つの TestSuite インスタンスとしてまとめれば便利でしょう.TF では,このことを行うのに 各テストクラスに suite クラスメソッドという TestSuite オブジェクトのファクトリメソッドを用意するというスタイルをとっています.
例えば, JUnit の場合次のようになります.
public static Test suite() { return new TestSuite(SampleTest.class); }
CppUnit の場合は次の通りです.
Test* SampleTest::suite() { TestSuite* suite = new TestSuite; suite->addTest(new TestCaller<SampleTest>("testA", testA)); suite->addTest(new TestCaller<SampleTest>("testB", testB)); return suite; }
例によって VBUnit の場合は特殊です.インスタンスメソッドとして,Suite メソッドがありますが,自分自身のオブジェクトを返しているだけです.これは,後述の TestRunner へのインターフェイスとして用意されているだけのようです.
VBUnit を除けば,テストクラスを作る場合, 自分自身についての suite クラスメソッドを作る ことになります.
Builder パターン
次に,テストを行った結果をレポートすることを考えましょう.テストクラスを実行しながらその場で直接テスト結果を標準出力に表示するというような方法では,汎用性がありません.やはりテスト結果自体をクラスにしてしまうのがよいでしょう.
テストを行った結果,テスト結果を表す TestResult オブジェクトができる,と考えれば,これはちょうど Builder パターンに相当します.TestResult オブジェクトの入れ替えるだけで TF を拡張することができます.例えば標準出力に表示するもの,GUIに出力するもの,HTML ファイルとして出力するもの,などが考えられます.
上図で TestRunner クラスは Builder パターンの Director の役割を果たします.TestRunner は まず TestResult オブジェクトを作成し, suite メンバとして登録されている Test オブジェクトの run メソッドに 作成したオブジェクトを渡します.その後 Test オブジェクトのツリー構造を横断しながら, 各テストコードが TestResult オブジェクトに addError や addFailure メソッドでエラー項目を追加します.
また,ConcreteTestResult クラスとしては TestRunner にエラー追加時のイベント等をフィードバックするための UITestResult クラス,標準出力に表示するための TextTestResult などが用意されています (JUnit).
実際には,TF のユーザが TestResult クラスをあまり意識することはありません.TestCase クラスには, assert メソッドが各種用意されています.テストクラスの runTest メソッド内では,いろいろな種類の assert メソッドを使ってテストコードを記述します.アサーションが失敗した場合,assert メソッドから例外が発生し,TestResult オブジェクトにエラー項目が追加されるという仕組みになっています.
TestRunner クラスが実際にテストを実行します.したがって, TestRunner にテストクラスを指定してテストを実行する ことになります.
TestRunner の使い方
TF を使ってテストを実行したい場合には, TestRunner にテストクラスを指定します.例えば JUnit でテストを実行したい場合は次のようにします.
public static void main(String args[]) { junit.textui.TestRunner.run(suite()); }
JUnit では TestRunner の GUI 版 ( junit.ui.TestRunner ) も用意されています.これは直接 junit.ui.TestRunner を java プログラムとして起動し,テストクラス名を入力するだけでそのテストクラスのテストを実行できます(正確には,そのテストクラスの suite クラスメソッドが返す TestSuite オブジェクトに対してです.したがって,前述の suite クラスメソッドが必要です).
上は,JUnit に付属しているサンプル, junit.samples.money.MoneyTest でテストしたときの画面です.
Failure と Error
TF では,エラーの種類を Failure と Error の 2 つに区別しています.Failure とは,アサーションによるチェックを行って失敗した場合を指し, Error とは,チェックをしなかった予想外のエラーのことを指します.TestCase クラスの assert メソッドによるアサーションが失敗すれば addFailer が呼ばれ,その他のエラーでは addError が呼ばれるという仕組みになっています.
上図は,JUnit に付属しているサンプル SimpleTest クラスを TestRunner で実行した結果です. 0 の割り算を行っている testDivideByZero 内で Error が起こっています(このメソッド内ではアサーションはいっさいありません).
assert メソッドは, assert と 変数を比較する assertEquals が用意されています.また Failure をわざと発生させるための fail メソッドも場合によって使うことになるでしょう.
Decorator パターン
TF では,テストクラスを拡張するのに Decorator パターンを使っています(VBUnitを除く).
ConcreteTestDecorator としては, ReaptedTest, TestSetup などが用意されています.例えば,同じテストを繰り返し 100 回 実行したい場合, RepeatedTest という Decorator を使います.
Test test = new RepeatedTest(SampleTest.suite(), 100); junit.textui.TestRunner.run(test);
もう一つの TestDecorator である TestSetup は,setUp と tearDown のコードを共有したい場合に使えます.例えば,テスト実行の前処理として データベースのオープン,後処理として データベースのクローズ を行いたい場合には,TestSetup をサブクラス化した TestDBSetup クラスを用意します.このクラスの setUp メソッドには データベースをオープンする処理, tearDown メソッドには データベースをクローズする処理になるようオーバーライドした後,
Test test = new TestDBSetup(SampleTest.suite()); junit.textui.TestRunner.run(test);
とすれば,SampleTest.suite() が返す TestSuite オブジェクトの実行前と実行後にデータベースのオープンとクローズが行われるようになります.他にも テストの実行を別スレッドとして実行するための ActiveTest という TestDecorator もあります.
VBUnit(Beta 1) では,TestDecorator は用意されていませんが,おそらく Event ステートメントを使えば TestDecorator を実装できると思います.
まとめ
以上を踏まえて,実際に TF を利用する場合の手順をまとめておきます.
- TestCase クラスのサブクラスを作る
- Fixture を表すインスタンス変数を追加する
- Fixture の初期化コードを setUp メソッドに, 終了コードを tearDown メソッドに書く
- assert を使って, testXXXX という名前のテストメソッドを作る
- 各テストメソッドを集めた suite クラスメソッドを作る
- TestRunner でテストを実行
興味をもたれた方は,実際に使ってみることをお勧めします.開発者としては,プログラムを書きながら同時にテストクラスを書く習慣をつけ,よりバグの少ない安定したソフトウェアを作るよう心がけたいものです.
この記事では TF をデザインパターンを使って解説しました.デザインパターンでフレームワークの構造を理解するのは非常に有益なことです.デザインパターンを知っている開発者に TF の概略を伝えるは,次のように説明するだけで十分でしょう.
- 基本は Command パターンである
--- テストクラスである TestCase が Command クラス
- テストの準備と後片付けに Template Method パターンを使っている
--- TestCase.run メソッドは setUp, runTest, tearDown の順に実行される
- 個々のテストをまとめて扱うことができるよう Composite パターンを使っている
--- TestSuite クラスが Composite クラス
- テストコードのインターフェイスの統一するため, Adapter パターンを使っている
--- テストメソッド を runTest メソッドに変換した Adapter が簡単に作れる
- テスト結果を生成するために Builder パターンを使っている
--- テスト結果を表す TestResult を TestRunner に登録する
- テストクラスを拡張するため Decorator パターンを使っている
--- TestDecorator をテストクラスにかぶせる
デザインパターンを理解していれば,フレームワークを構造を理解するのは簡単です.デザインパターンの威力がわかっていただけたでしょうか.