JUnitでTheoriesを使った大量のパラメータのテストで失敗した箇所があったとき(Eclipseで)少しわかりやすくする

タイトル通りです。コードから読み取ってください。

※タイトルが少し正しくなかったので直しました

サンプルコード

足し算をするロジックのテストをしたいと考えます。

package myapp.fixturesample;

public class MyLogic {
	public static int add(int lhs, int rhs){
		return lhs + rhs;
	}
}

これに対するテストコードを書きます。Fixtureクラス分けろよって話もそうですが、とりあえず。

package myapp.fixturesample;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;

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

@RunWith(Enclosed.class)
public class MyLogicTest {
	@RunWith(Theories.class)
	public static class AddTest{
		@DataPoints
		public static Fixture[] FIXTURES = {
			new Fixture(1, 1, 2),
			new Fixture(1, 2, 3),
			new Fixture(2, 1, 3),
			new Fixture(2, 2, 4),
			new Fixture(99, 99, 188), // oops
		};
		@Theory
		public void addTest(Fixture f){
			assertThat(MyLogic.add(f.lhs, f.rhs), is(f.expect));
		}
	}

	@Ignore("its fixture class")
	public static class Fixture{
		public final int lhs;
		public final int rhs;
		public final int expect;
		public Fixture(int lhs, int rhs, int expect) {
			this.lhs = lhs;
			this.rhs = rhs;
			this.expect = expect;
		}
	}
}

問題点

失敗したときのスタックトレースが分かりにくい。

org.junit.experimental.theories.internal.ParameterizedAssertionError: addTest("myapp.fixturesample.MyLogicTest$Fixture@61e4705b" <from FIXTURES[4]>)
 〜中略〜
Caused by: java.lang.AssertionError: 
Expected: is <188>
     but: was <198>
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:8)
	at myapp.fixturesample.MyLogicTest$AddTest.addTest(MyLogicTest.java:27)
 〜以下略〜

なるほど、なるほど、27行目が間違えて・・・
あ、これちがう、FIXTURESの4番目が間違えて・・・あ、5番目が間違えてたのかー。

ってなことを500個もある中から探すのは大変だよなァ!?アァン?

解決策

ちょっと汚いけどこうする。

package myapp.fixturesample;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Ignore;
import org.junit.experimental.runners.Enclosed;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class MyLogicTest {
	@RunWith(Theories.class)
	public static class AddTest {
		@DataPoints
		public static Fixture[] FIXTURES = {
				new Fixture(1, 1, expect(2)),
				new Fixture(1, 2, expect(3)),
				new Fixture(2, 1, expect(3)),
				new Fixture(2, 2, expect(4)),
				new Fixture(99, 99, expect(188)),
		};

		@Theory
		public void addTest(Fixture f) {
			assertThat(MyLogic.add(f.lhs, f.rhs), is(f.expect));
		}

		public static Matcher<Integer> expect(final int value) {
			final StackTraceElement ste = Thread.currentThread().getStackTrace()[2]; // expect()を呼び出している箇所がとれる

			return new TypeSafeMatcher<Integer>() {
				@Override
				protected void describeMismatchSafely(Integer item, Description mismatchDescription) {
					super.describeMismatchSafely(item, mismatchDescription);
					mismatchDescription.appendText("\n");
					mismatchDescription.appendText("==  failed fixture == \n");
					mismatchDescription.appendText("at ").appendText(ste.toString()).appendText("\n");
					mismatchDescription.appendText("==  original stacktrace ==");
				}

				@Override
				public void describeTo(Description description) {
					description.appendValue(value);
				}

				@Override
				protected boolean matchesSafely(Integer item) {
					return item != null && item.equals(value);
				}
			};

		}
	}

	@Ignore("its fixture class")
	public static class Fixture {
		public final int lhs;
		public final int rhs;
		public final Matcher<Integer> expect;

		public Fixture(int lhs, int rhs, Matcher<Integer> expect) {
			this.lhs = lhs;
			this.rhs = rhs;
			this.expect = expect;
		}
	}
}

やったことは以下。

  • Fixtureに使ってる期待値をintからassertThatとかで使うMatcherを使うようにした
  • 独自のMatcherを返すstatic methodを書いた
    • 内部でスタックトレースからメソッドを呼び出した元の情報を取って、Matcherで失敗したときに表示するようにして返してる。


そうすると、失敗時のスタックトレースはこうなる

org.junit.experimental.theories.internal.ParameterizedAssertionError: addTest("myapp.fixturesample.MyLogicTest$Fixture@1376c05c" <from FIXTURES[4]>)
 〜中略〜
Caused by: java.lang.AssertionError: 
Expected: is <188>
     but: was <198>
==  failed fixture == 
at myapp.fixturesample.MyLogicTest$AddTest.<clinit>(MyLogicTest.java:26)
==  original stacktrace ==
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:8)
	at myapp.fixturesample.MyLogicTest$AddTest.addTest(MyLogicTest.java:31)
〜以下略〜

見てもらいたいのはここ

==  failed fixture == 
at myapp.fixturesample.MyLogicTest$AddTest.(MyLogicTest.java:26)

何が嬉しいのかというと、Eclipse上でJUnit走らせたときに結果が出てくるウィンドウからコードジャンプが出来て失敗した行に1発で飛べて便利なんですねえ。

(該当行の左にスタックトレースのマークが出てないけど。仕事で使ってた時は出た気がするんだけどなー、よくわからん。)

終わり

これに気づいたとき、俺天才かって思った。
けど、今時じゃない感じがするし、多分今のテストの書き方も良くないんだと思う。

どういう値を指定した結果どうなったのか、とかわからないし・・・いやコードジャンプすれば一発ですよ・・・?あ、eclipseなんて知らない、そうですか・・・