テストを作る事は良くあると思いますが、
書いている時はよいのですが、後で見返して、これは何のテストだろーってなる事はあるあるだと思います。
今回は、そんな「何をしたかったのか分からな~い」をどれだけ緩和できるかにチャレンジしたいと思います。
いきなり結果から見せちゃいます。
左ペインにはTestNGのレポートが出ています(エラーはわざとですよ!!)。
右ペインは実際に動かしたテストソースです。
解説
まず、左ペインを見てもらえれば分かると思いますが、
今回はテストケース1つ1つを人の読める形で出力させています。(今回は日本語ですが、英語でもフランス語でも平気です)
こうする事で、どういったテストが成功して、どういったテストが失敗したかがすぐに分かるようになります。
レポートとして日々確認する必要がある場合は、何が失敗したかがすぐ分かる事は結構重要だったりします。
(作成者の名前も併せるとチーム開発の現場などでは報告しやすくなります。名前の入れ方はこの後の作り方を見て頂ければ拡張方法も分かると思います。)
とはいえ、これを作るのが大変だと本末転倒ということで、右ペインに移ります。
見て頂きたいのは、1つ1つのテストは、
situation("テストのタイトル", () -> {
テストの内容
})
という少ないフォーマットで書けるようになっているとうことです。
また、タイトルをテスト個々の頭に持ってくることで、何をしているのかを瞬時に判断しやすいようにしました。
実装方法
では、実際にこれをどう実現しているかを見てみます。
必要なのは、TestNG と、下記 「Situation.java」、「TestFrame.java」となります。
あとは、「TestFrame.java」を実装したテストケースを個々に考えて作ってください。
package test.test.test.test;
/**
* Case という名前だと予約語と被って色々面倒なので Situation.
* テストケースとして、title と 実行する test を記述した Runnable を保持しています.
*/
public class Situation {
private final String title;
private final Runnable test;
public Situation (String title, Runnable test) {
this.title = title;
this.test = test;
}
/**
* Runanalbe を実行しているだけ.
*/
public void test() {
test.run();
}
/**
* TestNG の結果には、DataSouce で渡される引数を toString() した物が出るようなので、タイトルを出力するようにする.
*/
@Override
public String toString() {
return title;
}
}
基本的には、JavaDocコメントにある通りです。
大事なのは、toString() でタイトルを返すこで、TestNGで実行した際のレポートにタイトルを表示させるようにしている事です。
package test.test.test.test;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
* abstract class に @Test と @DataProvider を実装しておき、
* {@link Situation} の配列を取得する部分のみサブクラスで実装するようにしています.
*/
public abstract class TestFrame.java {
/**
* {@link Situation} の実行のみ行う.
*/
@Test(dataProvider = "datasouce")
public void test(Situation situation) {
situation.test();
}
/**
* 以下の様な {@link Situation} の二次元配列を生成し、{@link DataProvider} として返してます.
* <br>
*
* new Situation[] {
* { new Situation(...) },
* { new Situation(...) },
* ...
*
* }
*/
@DataProvider(name = "datasouce")
public Object[][] source() {
Situation[] cases = situations();
Object[][] sources = new Object[cases.length][1];
for (int i = 0; i < cases.length; i++) {
sources[i][0] = cases[i];
}
return sources;
}
public Situation situation(String name, Runnable test) {
return new Situation(name, test);
}
/**
* テストケースとして、{@link Situation} の配列を返してください.
*/
protected abstract Situation[] situations();
}
こちらも基本的な事は、JavaDoc コメントに書いてあります。
ここでは、@Test、@DataProvider をここに書くことで、このクラスを継承するサブクラスが自動的にテストを規定動きに沿って処理させられるようにしています。
あとは、TestFrame.java を継承したサブクラスをつくって、そこに Situation をいっぱい追加してあげれば完成です。
package test.test.test.test;
/**
* {@link TestFrame} の実装.
* テストケースとして、{@link Situation} を生成して返します.
*/
public class TestImpl extends TestFrame {
/**
* ここにテストを実装.
*/
@Override
protected Situation[] situations() {
return new Situation[] {
situation("1 + 1 ", () -> {
int i = 1;
i = i + i;
System.out.println(i);
}),
situation("Fail するよ", () -> {
org.testng.Assert.fail("正しくエラーになりました.");
}),
situation("ラムダ式で書かない場合.", new Runnable() {
@Override
public void run() {
int i = 1;
i = i + i;
System.out.println(i);
}
}),
situation("改行することも"
+ "一応できます.", () -> {
int i = 1;
i = i + i;
System.out.println(i);
}),
};
}
/**
* {@link TestFrame} を実装することで、インナークラスでも動作可能.
*/
public static class Impl2 extends TestFrame {
@Override
protected Situation[] situations() {
return new Situation[] {
situation("インナークラスにしてみる.", () -> {
int i = 1;
i = i + i;
System.out.println(i);
})
};
}
}
}
TestNGという既存フレームワークにちょっと手心を加えてみました。
Jenkins の TestNG プラグイン を使って、CI環境に組み込むとより効果を発揮してくれるかと思います。