JUnit のセカイ #JJUG
このエントリーは、@cero-tさんのエントリーの次で、Java Advent Calendar 2011の6番目のエントリーです。自分自身の今年のメインテーマがTDD(テスト駆動開発)と言う事もあり、関連エントリーとしてJUnitについて書きたいかと思います。今更JUnit?と思われた方も普段からJUnitを使っていあなたも気軽にお読みください。尚、色々な話題を駆け足で紹介するので、どれも簡単な紹介程度になってしまいますが、ご了承願います。
JUnit4 スタイル
JUnitがアノテーションに対応し結構な月日が流れましたが、古いコーディング規約のままでテストコードを書いていませんか?JUnit4では、アノテーションとアサーションを使ったテストコードを書くことが基本スタイルです。かつては、TestCaseのサブクラスを作り、testではじまるメソッドを定義していましたが、今は Testアノテーションを付与したメソッドがテストメソッドとします。また、Assert#assertThat を使い自然言語(英語)風に書くのがJUnit4 のスタイルです。
public class CalcuratorTest { @Test public void addに3と4を与えると7を返す() throws Exceptioon { // setUp Calcurator sut = new Calcurator(); int expected = 7; // Excercise int actual = sut.add(3, 4); // Verify assertThat(actual, is(expected)); } }
このように、4フェイズテストを意識し、テストメソッドに日本語を使い解りやすくすると良いでしょう。
Matcher
Matcherは、アサーション時に期待値と実測値が等しいことを検証するためのフレームワークです。Javaでは一般的にequals メソッドを使った比較検証が行われます。equalsの検証だけで問題がないのであれば、CoreMatchers#is メソッドを使用すれば良いでしょう。
しかし、複雑なオブジェクトの検証を行う場合、単純なequalsの比較では情報が不足する事があります。例えば、何十行にも及ぶテキストを比較検証する時に、単純な文字列の比較だけでは一致しない事は解っても、何行目が一致しないかを地道に調べる必要が生まれます。そのような時には、カスタムMatcherの出番です。比較検証を行った上で、詳細な情報を出力する事ができます。
コンテキストベースのテスト
ユニットテストになれてくると、テストコードはテストの前提条件によってグルーピングする方が見通しが良くなると気付きます。例えば、データベースのテストをするのであれば、データベースが空の場合、1件のレコードがある場合、2件のレコードがある場合などに分け、それぞれのコンテキストで各メソッドのテストを実行するでしょう。そのような場合には、Enclosed テストランナーを利用したコンテキストベースのテストが便利です。
コンテキストベースのテストでは、static インナークラスを作成し、各インナークラスがテストクラスになります。それぞれでsetUpが行えるため、効率が良く見通しも良いテストコードが実現できます。
@RunWith(Enclosed.class) public class ItemDaoTest { public static class データベースが空の場合 { @Before public void setUp() { // setup } @Test public void getListのテスト() {} @Test public void findのテスト() {} } public static class データベースに1件のレコードがある場合 { @Before public void setUp() { // setupとデータを1件投入 } @Test public void getListのテスト() {} @Test public void findのテスト() {} } }
テストケースが増えてくれば増えてくるほど強力な書き方です。
テストフィクスチャを工夫する
テストではテスト用のデータが重要な要素です。しかし、Javaは柔軟な記述を行える言語ではありませんので、データをどう管理するかは1つの課題です。色々な手段はありますが、強引にJavaで宣言的にデータの初期化を行う方法を紹介します。
@Before public void setUp() { Item aItem = new Item() { { id = 10; name = "Book"; price = 1580; author = "hoge2"; } }; }
外部定義ファイルで良いのであればYamlを使い snakeYaml を使ってロードすると良いでしょう。
public static Fixtures load(InputStream input) { return (Fixtures) new Yaml(input).load(); }
パラメータ化テスト
テスト対象のクラスによっては、同じメソッドを様々な値で検証したい場合があります。このような場合、コピペを繰り返してテストを書く事もできますが、パラメータ化テストを行うと綺麗に書くことができます。JUnit ではコンストラクタでテストパラメータを受け取る方法と、テストメソッドの引数にテストパラメータを受け取る方法がありますが、ここではテストメソッドの方を紹介します。
@RunWith(Enclosed.class) public class JyankenTest { @RunWith(Theories.class) public static class 引き分けになるパターン { @DataPoints public static Jyanken.Hand[][] getParameters() { return new Jyanken.Hand[][] { {Jyanken.Hand.GU, Jyanken.Hand.GU }, {Jyanken.Hand.TYOKI, Jyanken.Hand.TYOKI }, {Jyanken.Hand.PA, Jyanken.Hand.PA } }; } @Theory public void judgeは0を返す(Jyanken.Hand[] hands) throws Exception { String msg = hands[0] + " vs " + hands[1] + " should be even."; assertThat(msg, sut.judge(hands[0], hands[1]), is(0)); } } }
パラメータ化テストでは、どのパラメータがエラーになったかを解らないため、assertThatの第1引数にメッセージを渡すなどの工夫が必要です。
assumeThat
assumeThatはassertThatとほぼ同じ構文をとる検証メソッドですが、assumeThatではマッチしない場合にそのテストは失敗とならずにスキップされます。この性質を利用すると、特定の環境(Windowsのみなど)で実行するテストを書く事ができたり、パラメータ化テストにおいてパラメータは全組み合わせを行う一方で、テスト毎に特定の条件を指定してパラメータのフィルタリングを行うなどの手法をとることができます。
Rule と ClassRule
Rule と ClassRuleは強力なJUnit の拡張フレームワークです。Rule を使う事により、各テストの実行前や実行後にAOPのような感覚で処理を挟む事ができます。例えば、外部アプリケーション(サーバやRDB)の起動や初期化処理などです。他にもJUnit のメタデータにアクセスできるため、テストをより細かくコントロールする事ができます。
Rule は自分で作成する事でユニットテストのコードを大きくリファクタリングできますが、デフォルトでいくつかのRuleも提供されています。必ず破棄されることが保証されるTemporaryFolder、テスト実行後の事後条件を検証するVerifier、実行中のテストメソッドの名前を取得できるTestNameなどです。
public class TemporaryFolderExcampleTest { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); @Test public void mkDefaultFilesで2つのファイルが作成される() throws Exception { File folder = tempFolder.getRoot(); TemporaryFolderExcample.mkDefaultFiles(folder); String[] actualFiles = folder.list(); Arrays.sort(actualFiles); assertThat(actualFiles.length, is(2)); assertThat(actualFiles[0], is("UnitTest")); assertThat(actualFiles[1], is("readme.txt")); } }
カテゴリ化テスト
テストケースが増えてくるとテストの実行に時間がかかり、フィードバックが遅くなる問題(スローテスト問題)にあたります。そのような時には、Category の機能を使い、テストにタグ(カテゴリ)を付け、カテゴリによって実行する(しない)テストケースを選択します。次の例では、SlowTestsのタグが付いたテストはCategorizedTestの実行時にはスキップされます。
@RunWith(Categories.class) @ExcludeCategory(SlowTests.class) @SuiteClasses({ FooTest.class, BarTest.class }) public class CategorizedTest { } public interface SlowTests { } @Category(SlowTests.class) public class FooTest { @Test public void test01() throws Exception { System.out.println("FooTest#test01"); } @Test public void test02() throws Exception { System.out.println("FooTest#test02"); } } public class BarTest { @Test public void fastTest() throws Exception { System.out.println("BarTest#fastTest"); } @Category(SlowTests.class) @Test public void slowTest() throws Exception { System.out.println("BarTest#slowTest"); } }