日々常々

ふつうのプログラマがあたりまえにしたいこと。

JUnit のテストメソッドを複数スレッドで実行する

JUnit4.8 のテストは、ぐるぐる巻きにされた Statement を転がすゲームです。

ご存知 @Before, @After, @BeforeClass, @AfterClass も @Test も @Rule も全部 Statement に変換されて、それらの Statement でテストメソッドを丁寧にラッピングしてから実行します。
@Rule はテストコードで自由に追加できますが、MethodRule の実装時*1に base.evaluate() を忘れると、全テストが無条件に通ったりします。前述の通り @Rule も Statement になり、その内側にテストメソッド他の Statement を持つ感じになるため、内側を呼ばなければテスト自体が実行されない…つまりエラーになりようもないのです。


ここまでを前提知識として、ちょっと応用。
@Rule を使うと、テストメソッドの実行タイミングを制御できます。書き忘れたらテストメソッド自体が呼ばれない……つまり0回になります。と言うことは、evaluate を2回書けば2回呼ばれます。その呼び出しをスレッドで実行すれば、テストメソッドをマルチスレッドで実行する事になります。テスト対象インスタンスをクラスフィールドに置くとかすれば、スレッドセーフじゃないのを検出できるかもしれません。

実装イメージ。ざっくりこんな感じ*2で Rule を書いてー…

2020-02-21追記
脚注に不安そうなことを書いている通り、Executors.newCachedThreadPool()の使用方法を誤っています。Executorsをこの位置で使用するならnewSingleThreadExecutor を使用した方が適切で(cachedでcacheされたスレッドを使用することがないため)、コンストラクタでスレッド数を受け取るならコンストラクタでnewFixedThreadPoolを使用した方が良いです。100スレッドとかプールするのもどうなのよって感じだし意味もないので、コア数やメモリを勘案して適切なサイズに調整するべきでしょう。さらにExecutorServiceがshutdownされていません。
このコードは動きはしますが、使用するにしても限定的な条件(クラッシュしても大事故にならない)にするべきです。決してプロダクトコードでこのようなExecutorの使い方をしてはいけません。

public class ThreadRule implements MethodRule {
	private final int count;

	public ThreadRule(int count) {
		this.count = count;
	}

	public Statement apply(final Statement base, FrameworkMethod method,
			Object target) {

		final Runnable run = new Runnable() {
			public void run() {
				try {
					base.evaluate();
				} catch (Throwable t) {
					throw new RuntimeException(t);
				}
			}
		};

		return new Statement() {
			@Override
			public void evaluate() throws Throwable {
				ExecutorService es = Executors.newCachedThreadPool();
				Future<?>[] fa = new Future[count];
				for (int i = 0; i < count; i++) {
					fa[i] = es.submit(run);
				}
				for (Future<?> f : fa) {
					f.get();
				}
			}
		};
	}
}

こんな風にテストする。これだと100スレッドになります。

public class RuleTest {

	@Rule
	public ThreadRule r = new ThreadRule(100);

	@Test
	public void test1() {
		System.out.println(Thread.currentThread().toString() + " test1");
	}

	@Test
	public void test2() {
		System.out.println(Thread.currentThread().toString() + " test2");
	}
}

この程度のテストで何が解るかと聞かれてもちょっと悩むし、UnitTestとしてどうなのよとかその辺は思うんだけど、マルチスレッドっぽい試験をJUnitで出来る気がして、それ以上に単にやってみたかったから、やってみました。一回書いちゃえば、やりたいテストに @Rule フィールドを書くだけ。むしゃくしゃした時とか、書いたコードをいじめるのには使えるかもしれません。

タイトルを見て「複数のテストメソッドを複数スレッドで実行してテスト時間を短くする方法」かと思ったりした方には、ごめんなさい。

*1:実際は Statement の実装時

*2:Executorは聞いたことあるけど、使ったのは初めて。よく判らないけど、何となく駄目実装になってることはわかる。joinする為にget使ってるけど、使い方的におかしい気もする。そのうち調べときます…。エラーハンドリングは勘弁してください。