mokky14's IT diary

IT関係の仕事メモ、勉強会の感想など書いてます。

JUnitでTheoryを使ってみた

同じテストロジックで、入力値のバリエーションテストを行いたい場合、入力値と期待値をパラメータ化したテストケースが書けることを知ったので使ってみた。

参考: JUnit実践入門

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

以下、テストケースのサンプル。

package junit.theory;

import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

@RunWith(Theories.class)
public class TheorySampleTest {
	//  テスト条件を設定したクラス
	public static class Fixture {
		int a, b;
		int result;

		public Fixture(int a, int b, int result) {
			this.a = a;
			this.b = b;
			this.result = result;
		}

		@Override
		public String toString() {
			return "Fixture{" +
					"a=" + a +
					", b=" + b +
					", result=" + result +
					'}';
		}
	}
	//  テスト条件
	@DataPoints
	public static Fixture fixtures[] = {
		new Fixture(1,2,3),
		new Fixture(2,2,4),
		new Fixture(2,3,4), //wrong
		new Fixture(0,1,2), //wrong
		new Fixture(1,1,3), //wrong
		new Fixture(2,4,6),
	};
	//  テスト
	@Theory
	public void testAdd(Fixture f) {
		assertThat(f.a + f.b, is(f.result));
		System.out.println("execute:" + f);
	}
}

ポイントは、

  • テストランナーとして@RunWith(Theories.class)を指定。
  • テスト条件(入力値と期待値)のクラスを作成する。なお、上記サンプルのtoString()はなくてよい。
  • 上記テスト条件クラスの配列を作成する。この配列が試験バリエーションそのものになる。この配列はpublic staticな配列とし、@DataPointsアノテーションを付与する。
  • テストメソッドには@Theoryアノテーション(@Testは使わない)を付与し、テスト条件のクラスを引数で指定する形とする。テストメソッドは、引数で渡された条件を使用したテストとして実装する。

このサンプルでは使ってないけど、RunWith(Theories.class)のクラス内でも@Beforeとか、@BeforeClassとかのアノテーションメソッドは問題なく使用できる。
なお、テスト条件については、外部ファイルから読み込むことも出来るらしいが試してない。

パラメータバリエーションのテストでパラメータだけ違うような同じロジックを延々と書かなくて良いので便利なんだけど、弱点が2つある。
上記のテストを実行した結果。

execute:Fixture{a=1, b=2, result=3}
execute:Fixture{a=2, b=2, result=4}

org.junit.experimental.theories.internal.ParameterizedAssertionError: testAdd(fixtures[2])
	at org.junit.experimental.theories.Theories$TheoryAnchor.reportParameterizedError(Theories.java:183)
	at org.junit.experimental.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:138)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithCompleteAssignment(Theories.java:119)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:103)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithIncompleteAssignment(Theories.java:112)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:101)
	at org.junit.experimental.theories.Theories$TheoryAnchor.evaluate(Theories.java:89)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:77)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:195)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:63)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: java.lang.AssertionError: 
Expected: is <4>
     got: <5>

	at org.junit.Assert.assertThat(Assert.java:780)
	at org.junit.Assert.assertThat(Assert.java:738)
	at junit.theory.TheorySampleTest.testAdd(TheorySampleTest.java:45)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
	at org.junit.experimental.theories.Theories$TheoryAnchor$2.evaluate(Theories.java:167)
	at org.junit.experimental.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:133)
	... 23 more


Process finished with exit code -1

エラーになったパターンが"testAdd(fixtures[2])"のように出力されるので、3つ目のパターンでNGになったことが分かる。
が、3つ目でNGになったら、以降のパターンのテストは行われずに終了する。この点は残念。

もう一個の弱点は、Eclipse+Quick JUnit環境で、@TheoryアノテーションのテストケースメソッドはCtrl+0で実行出来ない。
なので、メソッド個別のテストではなく、Ctrl+F11キーでテストケース内の全テストを実行することになる。こちらはあまり大きな問題ではないかも。