Template Methodパターンの事例

2009年5月1日(金)
中川 三千雄

デザインパターンでテスティング・フレームワークを作る

 昨今、ソフトウエアの商用/オープンソースを問わず、フレームワークを使用したソフトウエア開発を行う機会が増えています。

 そういったフレームワークは、学ぶべき設計ノウハウやデザインパターンの適用事例の宝庫です。クラス名やメソッド名からも利用しているデザインパターンがイメージできることもあります。さらにオープンソースソフトウエア(以下OSS)フレームワークは、優秀なエンジニアが作成したソースコードを自由に入手して読むことができます。そのようなソースコードは設計思想、設計ノウハウやデザインパターンの活用方法を学ぶ良い教材となります。

 本連載では、身近なフレームワークに含まれるデザインパターンの適用事例を解説します。どのような問題を解決しているのか、どのような工夫がなされているのかをひもといていきますので、デザインパターンについて効率よく理解を深めることができるでしょう。

 初回は、テスティング・フレームワークの1つであるJUnit3.8.1(http://www.junit.org/)を事例に、Template Methodパターンを取り上げます。Template Methodパターンは、GoFのデサインパターンの1つで、振る舞いに分類されるパターンで、抽象クラスと具象クラスの関係をうまく活用したパターンです。

 抽象クラスで処理の型(テンプレートになるメソッド)を実装し、具象クラスでその処理の型について具体的な処理を実装します。JUnitのTestCaseクラスは、Template Methodパターンが適用されています。テストケースは、必ずTestCaseクラスを継承して実装します。このTestCaseクラスに焦点を当てて、どのようにTemplate Methodパターンが適用されているのか、どのような効果があるのかを分析して理解を深めます。

 なお、これ以降は説明の都合上、次の名称で説明します。
テストコード:テストケースを束ねるソースコード。JUnitではTestCaseクラスに相当。
テストケース:1テストケース。JUnitではTestCaseクラスのサブクラスで実装するテストケースメソッドに相当。

必要なフレームワークを考える

 なぜテスティング・フレームワークが登場することになったのかを考えてみましょう。

 JUnit等のテスティング・フレームワークが存在しなかったころは、ユニットテストを実施するために、テスト・ドライバーを作成していました。テスト・ドライバーとはテスト対象を呼び出して評価するための別のプログラムのことで、ほとんどの場合は開発者がテスト対象ごとに作成し、個別に実行されていました。

 このようなテスト手法には、次のような問題がありました。

○テストコードの問題
・人によって、テストコードの実装が異なる
・テストコードの品質にバラツキがある(新人だとバグを埋め込むことも……)
・前に実装したテストコードをコピーして再利用する(似たようなコードが増える)

○実施時の問題
・テストコードを1つずつ、人の手で実行しなければいけない
・回帰テストなど、再実施に時間がかかる

○実施結果の問題
・実施したテストケースの数がわからない
・成功/失敗のときのステータスの表現が統一されていない
・実施結果から問題個所の特定が難しい

 今回はこの問題の中から、JUnitのTestCaseクラスに関係の深い「テストコードの問題」を解決するためのフレームワークを作成します。


■テストコードには共通の振る舞いごとがある

 テストコードの実装は、人によるところもありますが、誰が実装しても大体同じ内容になります。整理すると、次のような共通点があります。

○1テストケースにおけるテスト実行の処理パターン
 パターン1.           テストケース実行処理
 パターン2.テスト前処理 → テストケース実行処理
 パターン3.           テストケース実行処理 → テスト後処理
 パターン4.テスト前処理 → テストケース実行処理 → テスト後処理

○次の処理は、テストケースごとに異なる。
 テスト前処理
 テストケース実行処理
 テスト後処理

○テスト結果を評価する処理をテストコードごとに持っている


■テストコードにはテストケースが1つ以上存在する
 テストコードは、評価対象となるソースコードと対になるように作成するのが一般的です。

 テストコードには、正常系と異常系のテストケースを実装する場合が多いです。よってテストコードには、1つ以上のテストケースが含まれることを想定できます。

■こんなフレームワークが必要!?
 以上のことをまとめると、問題を解決するには、次の特徴を持ったフレームワークが必要になります。

1.テストケースの実行順序を統一したい
 今回の場合は、 テスト前処理 → テストケース実行処理 → テスト後処理
2.具体的なテストケースの処理内容のみ実装したい
3.テスト結果を評価する共通処理をすべてのテストコードから利用できるようにしたい
4.テストコードは1つ以上のテストケースで構成される

フレームワークを設計する

 次に、フレームワークを実現するための設計を行います。

1.テストケースの実行順序を統一したい
2.具体的なテストケースの処理内容のみ実装したい

 テストケースの実行順序は、すべて同じ処理の流れになります。一方、テストケースの内容は固有の処理になります。前者を抽象クラス、後者を具象クラスとして実装し、継承を用いることで実現できます。

 下の実装例Aのように、同じ処理を抽象クラスとして実装しておきます。抽象クラスを継承して、具象クラスに必要な処理を実装するようにすれば、必要最低限の実装で済むことになります。

 テスト前処理やテスト後処理のように、具象クラスでも具体的な処理があるかわからない場合は、(1)のように空実装しておきます。空実装にしておけば、具象クラスで必要に応じてオーバーライドして処理内容を変更することができます。

 逆にテストケース実行処理のように、具象クラスで必ずオーバーライドして処理を実装してもらいたい場合は、(2)のようにabstract修飾子を付けておきます。
===================================
【実装例A】

// 抽象クラス
class abstract TestCode {

   public final void run() {
    setUp(); // テスト前処理
    testCase(); // テストケース実行処理
    tearDown(); // テスト後処理
   }

  protected void setUp() {}      ... (1)

  protected abstract void testCase(); ... (2)

  protected void tearDown() {}     ... (1)
}


// 具象クラス
class xxxTestCode extends TestCode {

  protected void setUp() {
    // 必要があれば、その処理内容を実装する
  }

  protected void testCase() {
    // 具体的なテストケースの処理内容を実装する
  }

  protected void tearDown() {
    // 必要があれば、その処理内容を実装する
  }
}
===================================

3.テスト結果を評価する共通処理をすべてのテストコードから利用できるようにしたい
 抽象クラスに共通処理を実装することで具象クラスから利用できます。

4.テストコードは1つ以上のテストケースで構成される
 テストケースの実装は、抽象クラスのTestCaseメソッドをオーバーライドして実現するつもりでした。しかし、それでは図2のBeforeのように、テストコードに1つのテストケースしか実装することができません。

 そこで、少し視点を変えて考えます。

 図2のAfterのように、テストを実施するときにテストケース名を指定して実行でき
るようにします。そうすれば、具象クラスにテストケースが何件あっても対応できます。

フレームワークの完成

 いよいよ、デザインパターンを用いてフレームワークを完成させます。

■デザインパターンの適用
 Template Methodパターンを適用する場合、次のような実装を行うことになります。
 ・テンプレートメソッドを持った抽象クラスを実装する
 ・テンプレートメソッド内で呼び出しているメソッドは抽象メソッドとして実装される、あるいは具象クラスでオーバーライド対象になるものが多い
 ・共通処理の実装は抽象クラスで行う
 ・抽象クラスを継承した具象クラスは固有の実装のみ行う

===================================
【実装例B】

// 抽象クラス
public abstract class TestCode {
  private String methodName;

  public TestCode(String methodName) { ... }

  // テンプレート・メソッド
  public final void run() {           ... (1)
    setUp(); // テスト前処理
    invokeTestCase(); // テストケース実行処理
    tearDown(); // テスト後処理
  }

  protected void setUp() {}

  protected void tearDown() {}

  private void invokeTestCase() {         ... (4)
    // methodNameで指定した名前の
    // メソッドを間接的に実行するための実装を行う
  }

  // テスト結果を評価する共通処理
  protected void assertTrue( ... ) { ... }     ... (3)
  protected void assertEquals( ..., ...) { ... }  ... (3)
}

// 具象クラス
public class ArrayListTestCode extends TestCode {
  ArrayList list;

  public ArrayListTestCode(String methodName) { ... }

  protected void setUp() {           ... (2)
    ArrayList list = new ArrayList();
  }

  public void testIsEmpty() {          ... (2)(4)
    assertTrue(list.isEmpty());        ... (3)
  }

  public void testSize() {            ... (2)(4)
    list.add(new Object());
    assertEquals(list.size(), 1);       ... (3)
  }
}
===================================

■デザインパターンの効果
 今回の事例では、Template Methodパターンを適用することで、次の特徴を持ったフレームワークを作成することができました(上の実装例Bを参照してください)。

 (1)テスト実行時の実行順序を統一する
 (2)テストケースに必要な処理の実装のみを行う
 (3)テスト時に利用する共通処理をすべてのテストコードで利用できる

また、テスト時に実行したいテストケース名を指定してもらう仕様にしたことで、(4)のように1つのテストコードに複数のテストケースが実装できるようになりました。

===================================
【利用イメージ】
TestCode test;

test = new xxxTestCode("testIsEmpty");
test.run();

test = new xxxTestCode("testSize");
test.run();
===================================

JUnitのTestCaseクラスに関する補足

 今回の事例の中心は、TestCaseクラスでした。Template Methodパターンに関係する部分に焦点を当てて説明しました。そのため、関係のない部分の説明は割愛しています。

 実際は図3-1を見てわかるように、もう少し複雑な構成で実装されています。

 TestCaseクラスのテンプレートメソッドはrunBareメソッドです。図3-2を見てわかるようにrunBareメソッドは、setUpメソッド→runTestメソッド→tearDownメソッドの順に実行します。runTestメソッドで外部から指定されたテストメソッド(testCase1)を間接的に実行しているのがよくわかります。この工夫によって、複数のテストケースが実装できるようになりました。

 今回、説明した実現方法では、テストを実施する際に1つのテストケースに1つのインスタンスを生成する必要がありました。これは、各単体テストはほかのすべての単体テストから切り離して実行したいというJUnitのコンセプトなのです。ほかの単体テストの結果や状態に左右されないための配慮です。しかし、テストケースが増えるにつれて、最初に挙げた「テスト実施の問題」が次第に大きくなります。

 JUnitでは、その問題を解決するための手段としてTestSuiteクラスが用意されています。TestSuiteクラスはTestCaseクラスに実装されている"test"で始まるメソッドを内部検索します。そして、発見したメソッドを実行するためのTestCaseクラスのインスタンスを自動的に生成して保持します。

 TestSuiteクラスは、Testインターフェースを用いて保持するすべてのTestCaseクラスをまとめて実行することができます。

public class xxxTest extends TestCase {
  public void testCase1() { ... };
  public void testCase2() { ... };
  public void testCase3() { ... };
}

TestSuite suite = new TestSuite();
suite.addTest(xxxTest.class);

// testCase1~testCase3のテストケースをまとめて実行
suite.run(new TestResult());

 このように実際のフレームワークは、それぞれの責務を持ったクラス同士がコラボレーションし、フレームワークの目的を果たしているのです。

Template Methodパターンはオブジェクト指向設計の基本中の基本

 今回、紹介したTemplate Methodパターンはテンプレートという名前から想像できるように何かの型にはめるという意味がとても強いパターンです。プログラミングをしていると「処理はほとんど同じなんだけど、一部異なる」という場面によく遭遇します。

 そんなときは、慌てないで実装をいったん止めてください。そして、処理の共通部分と固有部分をよく見極めます。そうすることで抽象クラスが次第に見えてくるハズです。そこでTemplate Methodパターンを適用するのです。

 また、今回の事例のように、視点を変えて考えてみることはTemplate Methodパターンに限らず、すべてのデザインパターンに共通して言えることであり、非常に重要です。視点を変えることで、デザインパターンの適用の幅を広げることができるのです。

 このほかにもTemplate Methodパターンは、そのパターンの性質上、アプリケーション開発のためのフレームワークの中でよく使われています。

 次回は、引き続きJUnitを事例にObserverパターンを紹介します。それでは、次回をお楽しみに!

【参考文献】

Erich Gamma, Rechard Helm, Ralph Jonson, John Vlissides『オブジェクト指向における再利用のためのデザインパターン』ソフトバンククリエイティブ(発行年:1999)
結城 浩『Java言語で学ぶデザインパターン入門』ソフトバンクパブリッシング(発行年:2001)
Vincent Massol,Ted Husted『JUnitインアクション』ソフトバンクパブリッシング(発行年:2004)
「JUnit.org」(http://www.junit.org/) (アクセス:2009/04)

株式会社オージス総研
株式会社オージス総研アドバンストモデリングソリューション部アーキテクトチーム兼エンタープライズオープンソースセンター所属。これまでに2社の独立系SI企業で経験を積み、現在に至る。オージス総研では、フレームワークの設計/開発や開発プロセス支援など、アーキテクトとしてプロジェクトに参画する。また、オージス総研のコミュニティー「オブジェクトの広場」にも参加している。
オブジェクトの広場 http://www.ogis-ri.co.jp/otc/hiroba/

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています