JUnit 4.4から組み合わせを表現するためのTheoryという機能が導入されています。このTheoryという機能について、日本語での概説がなかったため、調べた範囲で紹介したいとおもいます。
Theoryの実行
Theoryは組み合わせテストのための表現方法です。Theoryを利用するためには、@RunWith
アノテーションでTheories
ランナーを指定します。すると、@Theory
アノテーションが指定されたメソッドが実行されます。
@RunWith(Theories.class)
public class HelloTheoryTest {
@Theory
public void helloTheory() {
System.out.println("Hello, Theory!");
}
}
出力は下記の通りとなります。
Hello, Theory!
DataPointの指定
@DataPoint
アノテーションを利用すると、@Theory
テストメソッドに引数として渡される値を指定することができます。@DataPoint
アノテーションは静的変数、もしくは静的メソッドに対して指定します。@DataPoin
が複数あれときには、それぞれが@Theory
テストメソッドの引数として渡され、その数だけ@Theory
テストメソッドが実行されることになります。
@RunWith(Theories.class)
public class HelloDataPointTest {
@DataPoint
public static String TARO = "Taro";
@DataPoint
public static String JIRO = "Jiro";
@DataPoint
public static String sabro() {
return "Saburo";
}
@Theory
public void helloDataPoint(String name) {
System.out.println("Hello, " + name + "!");
}
}
出力は下記の通りとなります。
Hello, Taro!
Hello, Jiro!
Hello, Saburo!
@DataPoints
アノテーションを使うと、複数の@DataPoint
を配列によってまとめることができます。
@RunWith(Theories.class)
public class HelloDataPointsTest {
@DataPoints
public static String[] NAMES = { "Taro", "Jiro", "Saburo" };
@Theory
public void helloDataPoint(String name) {
System.out.println("Hello, " + name + "!");
}
}
@Theory
テストメソッドにどのような@DataPoint
が渡されるかについては、メソッド仮引数の型に基づいて決められます。
@RunWith(Theories.class)
public class TypedDataPoint {
@DataPoints
public static int[] INTS = { 1, 2, 3 };
@DataPoints
public static String[] STRINGS = { "One", "Two", "Three" };
@Theory
public void methodWithInt(int n) {
System.out.println("#methodWithInt(int) -> " + n);
}
@Theory
public void methodWithString(String s) {
System.out.println("#methodWithString(String) -> " + s);
}
}
出力は下記の通りとなります。
#methodWithInt(int) -> 1
#methodWithInt(int) -> 2
#methodWithInt(int) -> 3
#methodWithString(String) -> One
#methodWithString(String) -> Two
#methodWithString(String) -> Three
@TheoryTest
メソッドに複数の引数があるとき、@DataPoint
として定義された値の全ての組み合わせについて、@TheoryTest
メソッドが実行されます。例えば、@DataPoint
としてグー、チョキ、パーの3種類が定義されていて、@TheoryTest
メソッドにふたつの引数があるとき、9通りの組み合わせが@Theory
テストメソッドの引数として渡され、テストが実行されます。
@RunWith(Theories.class)
public class JankenTest {
@DataPoints
public static string[] GESTURES = { "ROCK", "SCISSORS", "PAPER" };
@Theory
public void allPair(Gesture lhs, Gesture rhs) throws Exception {
System.out.printf("%-8s vs %s\n", lhs, rhs);
}
}
出力は下記の通りとなります。
ROCK vs ROCK
ROCK vs SCISSORS
ROCK vs PAPER
SCISSORS vs ROCK
SCISSORS vs SCISSORS
SCISSORS vs PAPER
PAPER vs ROCK
PAPER vs SCISSORS
PAPER vs PAPER
TheoryとAssume
ここまで、Theoryの基本的な動作について説明をしました。このTheoryを利用して、どのようにテストを行なうかを見ていきましょう。
具体例として、視聴者層の区分を考えます。視聴者層の区分とは、広告代理店などで利用されている区分で、F1層とかM1層とかいう区分です。詳しい説明はWikipediaの項目を読んでください。ここでは視聴者クラスとしえAudience
クラスを定義し、その区分を取得するgetCategory()
メソッドを実装します。
enum Gender {
MALE, FEMALE
}
enum Category {
C, T, F1, F2, F3, M1, M2, M3
}
class Audience {
private final Gender gender;
private final int age;
public Audience(Gender gender, int age) {
this.gender = gender;
this.age = age;
}
public Category getCategory() {
if (age < 13) {
return Category.C;
} else if (age < 20) {
return Category.T;
} else if (age < 35) {
return gender == Gender.MALE ? Category.M1 : Category.F1;
} else if (age < 50) {
return gender == Gender.MALE ? Category.M2 : Category.F2;
} else {
return gender == Gender.MALE ? Category.M3 : Category.F3;
}
}
}
このAudience
クラスのTheoryテストを次のように書いてみました。テストデータは同値分割やら限界値分析やらを考慮して、性別のデータと年齢のデータを@DataPoints
として定義します。そして、返却されるカテゴリーがF1層であるかどうかのテストを追加します。
@RunWith(Theories.class)
public class AudienceTest {
@DataPoints
public static int[] AGES = { 0, 12, 13, 19, 20, 34, 35, 49, 50, 99 };
@DataPoints
public static Gender[] GENDERS = { Gender.MALE, Gender.FEMALE };
@Theory
public void categoryIsF1(Gender gender, int age) throws Exception {
Audience audience = new Audience(gender, age);
assertThat(audience.getCategory(), is(Category.F1));
}
}
このテストは失敗します。というのも、性別と年齢との全ての組み合わせが、@Theory
テストメソッドに渡されるのでAssert
しているF1層以外のカテゴリーを返すAudience
オブジェクトも生成されるからです。そこで、F1層の組み合わせだけ、Assert
される仕組みが必要になります。
そのために、Assume
を利用します。Assume
は、テストを失敗することなく中止させます。次のソースコードと実行結果を見てください。
@RunWith(Theories.class)
public class HelloAssumeTest {
@DataPoints
public static String[] NAMES = { "Taro", "Jiro", "Saburo" };
@Theory
public void helloDataPoint(String name) {
System.out.println("Before: Hello, " + name + "!");
assumeThat(name.length(), is(6));
System.out.println("After: Hello, " + name + "!");
}
}
出力は下記の通りとなります。
Before: Hello, Taro!
Before: Hello, Jiro!
Before: Hello, Saburo!
After: Hello, Saburo!
assumeThat
の条件が真になった処理だけ、処理が継続していることが分かります。Assert
であれば条件が偽になったときにテストが失敗するのに対し、Assume
はテストを失敗させません。Assume
を利用すれば、指定した条件に合致しないときに、それ以降のテスト処理を実行せずに中止させることができます。
Audience
クラスのTheoryテストをAssume
を使って書き直してみましょう。F1層であるかというテストですので、Assume
の条件として女性であること、20歳から35歳の範囲であることを指定します。
@RunWith(Theories.class)
public class AudienceTest {
@DataPoints
public static int[] AGES = { 0, 12, 13, 19, 20, 34, 35, 49, 50, 60 };
@DataPoints
public static Gender[] GENDERS = { Gender.MALE, Gender.FEMALE };
@Theory
public void categoryIsF1(Gender gender, int age) throws Exception {
assumeThat(gender, is(Gender.FEMALE));
assumeTrue(19 < age && age < 35);
Audience audience = new Audience(gender, age);
assertThat(audience.getCategory(), is(Category.F1));
}
}
Theoryのメリット、デメリット
Theoryを使うことで、組み合わせ網羅のテストを自動化することができます。JUnitを利用してブラックボックステストを自動化するときには、強力な武器になるでしょう。
一方で、Theoryテストには不便な面もあります。一番不便な点といえば、テストがどのような値のときに失敗したのかを伝えてくれない点です。また、成功したテストケースであっても、全ての成功するケースを網羅できているかを検証できません。成功するテストケースとは別に、失敗するテストケースを成功するテストケースの補集合としてテストするなどの工夫が必要になります。