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 を書いてー…
脚注に不安そうなことを書いている通り、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 フィールドを書くだけ。むしゃくしゃした時とか、書いたコードをいじめるのには使えるかもしれません。
タイトルを見て「複数のテストメソッドを複数スレッドで実行してテスト時間を短くする方法」かと思ったりした方には、ごめんなさい。