assertEqualsするだけがJUnitじゃなかった!機能の紹介
ユニットテストって良いですよね!デグレード(リグレッション)に気付けたり、テストコードから仕様が読み取れたり、設計も改善されますし、リファクタリングに使えるし・・・色々良い感じです。
今日はユニットテストのツール、JUnitの話題です。
Javaプログラマには必須のライブラリ・フレームワークの1つであるJUnitですが、恥ずかしいことに、私の習熟度は低いです。理由は、過去にある程度使えるようになった時点で学習を辞めてしまったことと、仕事では色々な問題のせいで使う機会が少ないこと、の2点です。
今回、以下のなかなか良さげな本の存在を知ったのでちゃんと学習してみることにしました。
[tmkm-amazon]477415377X[/tmkm-amazon]
使えそうな良いと思った機能・テクニック
assertThat
自分が初めてJUnitに触れたときはこんな機能ありませんでした。そのため、JUnitと言えばassertEqualsという認識だったんですが、こっちのassertThatメソッドの方が断然良いですね!これからはこちらを使います!
assertThatはMatcherというオブジェクトを利用してマッチングを行う点がassertEqualsと大きく異なります。どのように違うかは以下の通りです。
例えばこのようなテストケースがあるとします。
package com.tsukaby.sample;
import java.util.Date;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class HogeTest {
@Test
public void test_取得した日付が正しいか() {
// 実際はテスト対象クラスのメソッドから取得する
// 2014/1/1 00:00:00
Date actual = new Date(114, 0, 1);
// 2014/1/1 00:00:01
Date expected = new Date(114, 0, 1, 0, 0, 1);
assertEquals(expected, actual);
}
}
(actualの値はテスト対象のメソッドから受け取ったことにしてください。ここでは例のため直接Dateをnewしていますが。)
このテストを実行すると当然レッドバー、テスト失敗です。なぜなら1秒ずれていますので。
Dateのような日付型を扱うとき、日未満の単位、つまり時、分、秒は何でも良い、拘らない、というシーンが多々あります。上記のテストで2014/1/1かどうかを検証するというコードを書く場合どうしたらいいのでしょうか。
以下は解の1つですが、微妙です。
assertEquals(expected.getYear(), actual.getYear());
assertEquals(expected.getMonth(), actual.getMonth());
assertEquals(expected.getDay(), actual.getDay());
このようなケースはassertThatを利用するとスマートに解決できます。assertThatはMatcherというオブジェクトを利用してマッチングを行います。そのため、このMatcherを利用シーンに合わせて選択すれば良いわけです。
Matcherはそれほど苦労することなく独自に作成することができますが、ここでは独自に作成することはしません。Dateは標準APIですし、きっと世の中のどこか、特にGitHubなんかにDateを検証するMatcherを公開している人がいるはずです。
これは実際居て、以下にて公開されています。
https://github.com/modularit/hamcrest-date
いやっほう!stewbisありがとう!Maven Centralにも登録されている!さらにBSDライセンスで僕たちは(比較的)自由だ!
早速上記のサイト通りpom.xmlに依存ライブラリを追加し、hamcrest-dateを利用します。
package com.tsukaby.sample;
import java.util.Date;
import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import uk.co.it.modular.hamcrest.date.DateMatchers;
@RunWith(JUnit4.class)
public class HogeTest {
@Test
public void test_取得した日付が正しいか() {
// 実際はテスト対象クラスのメソッドから取得する
// 2014/1/1 00:00:00
Date actual = new Date(114, 0, 1);
// 2014/1/1 00:00:01
Date expected = new Date(114, 0, 1, 0, 0, 1);
assertThat(actual, DateMatchers.sameDay(expected));
}
}
これはグリーンバーで成功します。sameDayは年、月、日までを検証するためです。2014/1/1と2013/1/1とかは勿論年度が異なるのでレッドになりますが、上記のような秒が異なるケースはグリーンになります。
assertThatとMatcher非常に便利です。
テストメソッド名は日本語で
これは前々から思ってたことなのですが、上記の本を読んで考えが固まりました。テストメソッドは日本語で書いていいんだ・・・!
業務では会社やプロジェクトの特性上、テストケースを書くことは少ないんですが、それでも少しはあります。そんなとき、テスト名は以下のように付けていました。
@RunWith(JUnit4.class)
public class HogeTest {
//ケース1、番号
@Test
public void test001(){
}
//ケース2、正常系か異常系か
@Test
public void testSuccess001(){
}
@Test
public void testFailure001(){
}
}
理由はテストケースを記述したExcel表が別にあるので、それと対応付けるために番号が必要だったり、少しは分かりやすくしようとSuccessとかは付けるようにしたりと、そんな感じです。
前提条件やテストの概要はJavadocコメントに頑張って書いていましたが・・・ずっと微妙な気持ちでもやもやしていました。(既に他の人が作ったサンプル実装を真似る必要があって簡単には改善できない状態でした)
テストメソッド名は日本語で書いていいんですね!
@RunWith(JUnit4.class)
public class HogeTest {
@Test
public void test_○○と○○を○○し、○○になる(){
}
}
頭の”test”は慣習なのでそのまま残して後ろに日本語名を付けて分かりやすくします。日本語部分は5W1Hとかを考えて、プロジェクトである程度方針を決めた方が良い気がします。
Enclosedテストランナー
テストクラスの中にインナークラスを作成する、というものです。
package com.tsukaby.sample;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
@RunWith(Enclosed.class)
public class HogeTest {
public static class HogeTest_名前リストに値が存在するケース {
private List<String> names = new ArrayList<String>();
@Before
public void setUp() {
names.add("Taro");
names.add("Jiro");
names.add("Tom");
}
@Test
public void test_1() {
//namesを使ってあるメソッドをテスト
}
@Test
public void test_2() {
//namesを使ってあるメソッドをテスト
}
}
public static class HogeTest_名前リストに値が存在しないケース {
List<String> names = new ArrayList<String>();
@Test
public void test_1() {
//namesを使ってあるメソッドをテスト
}
@Test
public void test_2() {
//namesを使ってあるメソッドをテスト
}
}
}
前提条件ごとに構造化できたりするので良い感じですね。
上記ではnamesをテスト対象のメソッドに与えることを想定しているので、各テストメソッド内でnamesを構築した方が良いかもしれませんが、DBの初期化を行う場合などは確実に「前提条件」と言えるので役立ちそうです。
パラメータ化テスト
テストケースを書くとき、テスト対象のメソッドに与えるパラメータとその予測値が異なるだけなのに、同じようなテストケースメソッドを何度も書かないといけないことがよくあります。そんなときはパラメータ化テスト(Theoriesテストランナー)を利用します。
例えば以下のようなで。
package com.tsukaby.sample;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class HogeTest {
@DataPoints
public static Fixture[] fixtures = {
new Fixture(2, 3, 5),
new Fixture(3, 4, 7),
new Fixture(5, 10, 15)
};
@Theory
public void test_パラメータ化テスト(Fixture fixture) {
int actual = Calculator.add(fixture.getX(), fixture.getY());
int expected = fixture.getResult();
assertThat(actual, is(expected));
}
private static class Fixture {
private int x;
private int y;
private int result;
public Fixture(int x, int y, int result) {
this.x = x;
this.y = y;
this.result = result;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getResult() {
return result;
}
public void setResult(int result) {
this.result = result;
}
}
}
これを実行するとテストが3回走ります。もし3つあるFixtureのうち、3つ目でエラーが発生した場合、
エラー発生: test_パラメータ化テスト(fixtures[2])というような表示が出るので、どこでエラーが出たか分かります。
他にも色々
上記の本では他にも様々なテクニックが公開されていました。
- Matcher APIのnotでassertThat(actual, is(not(expected)));と書ける 英文なので自然な形になる
- Categoriesテストランナーでテストをカテゴリー化して一部だけ実行 DBのテストだけは分けておいて、スローテスト問題解決
- ルール
- テストダブル(モックのライブラリを使ってテスタビリティ向上)
などなど。
非常に勉強になりました。JUnitの知識がバージョン3で止まっている人や、自分と同じくassertEqualsばっかり使っている人にはお勧めの1冊です。
JUnit4をマスターして頑張ってユニットテストが十分されている開発を目指します。