2013年8月12日月曜日

データベースのテスト支援ツール DbUnit その4 デザインパターン編

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

データベースのテスト支援ツール「DbUnit」の連載も今回で最終章です。

DbUnitの意義、基本的使い方につきましては前回まででご紹介済みです。
もう十分にDbUnitを有効活用していくことが出来るかと思います。

最終章の今回は、殆ど小ネタに過ぎない話ですが、デザインパターンについてご紹介します。

さて、DbUnitを用いたDB関連のテストは、概ね以下の構成を取ります。

  1. DBのセットアップ(テーブルのクリア、レコードの登録)
  2. テスト本体
  3. 終了処理(テーブルの掃除、セッションの切断)

ここで地味に問題になってくるのが、「DBのセットアップ」の処理の重さです。

「DBセットアップ=レコードのインサート」である為、この部分の処理に必要とする時間がオンメモリの処理と比べて重いのです。
一回一回のインサート処理ならばそんなに気にならない程度の重さですが、塵も積もれば山となる。
大きなプロジェクトで複数人が大量のDbUnitテストを作成した場合、段々段々と時間が掛かるようになり、仕舞いには一回のテスト実行で何時間も待たされたり、下手すれば一晩待っても終わらないです。
また、そんなに派手に遅くならない場合でも、ユニットテストは何度も繰り返すものですので、長い目で見れば馬鹿にならない時間を待ち時間に費やしていることになります。
これを「スローテスト問題」と言いまして、JUnit開発者の悩みの種です。

ユニットテストは、処理速度を意識しなければならない。

処理速度は、ユニットテストにおいて意外に大事な要素なのです。
今回のテーマはデザインパターンに加え、処理効率化についてもご紹介します。

デザインパターン


デザインパターンの要点は簡単です。
「Enclosedを使用し、DBのセットアップの種類に応じてグループ化すれば良い」です。

Enclosedについて忘れた方はこちら

/**
 * @author 技術開発事業部 遠藤 太志郎
 *
 */
@RunWith(Enclosed.class)
public class ShohinServiceTest {

 public static class テーブルが空の状態から始まるテスト {
  @Before
  public void setUp() throws Exception {
   //セットアップ処理(軽い)
  }
  @After
  public void tearDown() throws Exception {
   //終了処理
  }
  @Test
  public void testInsert() throws Exception {
   //テスト本体1
  }
  @Test
  public void testInsert2() throws Exception {
   //テスト本体2
  }
  @Test
  public void testInsert3() throws Exception {
   //テスト本体3
  }
  //以下続く……

 }

 public static class テーブルに100件のレコードが入っている状態から始まるテスト_select {
  @BeforeClass
  public static void setUpBeforeClass() throws Exception {
   //セットアップ処理(重い)
  }
  @AfterClass
  public static void tearDownAfterClass() throws Exception {
   //終了処理
  }
  @Test
  public void testFindById() throws Exception {
   //テスト本体1
  }
  @Test
  public void testFindById2() throws Exception {
   //テスト本体2
  }
  @Test
  public void testFindById3() throws Exception {
   //テスト本体3
  }
  //以下続く……
 }

}

こんな感じです。
この要に「テーブルの初期状態でグループ化」しておくと可読性が高まります。
DbUnitのデザインパターンとしては鉄板の構成と言えるでしょう。

そして、処理効率化については以下のテクニックを使用しています。
上のソース、実は二種類のセットアップ方法を使い分けています。

@Before
public void setUp() throws Exception {
 //セットアップ処理
}

@BeforeClass
public static void setUpBeforeClass() throws Exception {
 //セットアップ処理
}

そう、「@Before」と「@BeforeClass」を使い分けているのです。
正確に言えば、「@After」「@AfterClass」も使い分けています。

このアサーションの意味は以下です。

アサーション意味
@Before各テストメソッドの実行前に毎回呼び出される。
@BeforeClass各テストクラスの実行前に一回だけ呼び出される。
@Afterデータ各テストメソッドの実行後に毎回呼び出される。
@AfterClass各テストクラスの実行後に一回だけ呼び出される。

つまり、

  • 「select系のテスト」のようにテストによってDBの中身が変わらない場合は「@BeforeClass」を使う。
  • 「insert、update、deleteのテスト」のようにテストによってDBの中身が変わる場合は「@Before」を使う。

こうすることによって、「select系のテスト」におけるセットアップ処理の実行回数を減らしているわけですね。
「insert、update、deleteのテスト」は高速化出来ませんが、これは仕方ありません。

しかし、多くのシステムの場合、「selecte系テストの機能の数 > 他の機能の数」という構成の事が多いですので、
select系のテストだけ高速化しても結構な成果を上げることが出来るのです。

注意事項


しかし、これだけは明言しておかなければいけません。

「@BeforeClassはなるべく使うな」

こんな記事を書いておいて何だと思われるかもしれませんが、これは大事な事です。
JUnitのテストは「各テスト間で干渉しない」ということが絶対必須。
メソッド毎の独立性は高ければ高いほど良いのです。

このため、メソッド毎に初期化を行う「@Before」は独立性が高いのに対し、
クラスで一回しか初期化しない「@BeforeClass」は独立性が低いと言えるでしょう。

「@BeforeClass」はテストの品質を下げる諸刃の刃なのです。

この為、

  • 「どうしてもこれらのテスト群は処理が重いから@BeforeClassで高速化しよう」
  • 「こっちのテスト群はそこまでの重さではないから、@Beforeにしておこう」

と言うように、@BeforeClassは最終手段の特殊対応。
基本は@Beforeを使うように心がけて下さい。

終わりに


以上でDbUnitのご紹介は終わりです。

今までの記事ではJUnitの機能をオンメモリで使う例を挙げて来ました
しかし今の時代、大抵は「DBの登録、更新、削除、検索」がシステムの主な機能ということが殆ど。
オンメモリで終わるシステムなんぞ滅多に無いでしょう。
このDbUnitの使い方をマスターした段階で、ようやく現場で使えるレベルに入ったといった所かと思います。

次回からは新章、「モック編」です。
JUnit界最強と名高いモックツール「JMockit」についてご紹介します。

0 件のコメント:

コメントを投稿