JUnitのテストケースを並行で実行する
JUnitのテストケースを並行で実行したメモです。
JUnitのテストケースを並行で実行してみました。
JUnitで複数の組み合わせのパターンをテストするテストケースがある場合、
組み合わせが大量の場合だとかなり時間がかかってしまうので、並行で実行出来るようにしたいです。
組み合わせのパターンのテストの場合、パラメータ化テストで実行も出来ますが、並行では出来ないです。
JUnitでパラメータ化テストを実施する - abcdefg.....
TestNGというテスティングフレームワークを使用すると、threadPoolSizeを指定することで
マルチスレッドでテスト出来るようです。
@Test(threadPoolSize = 3)
JUnitでも工夫すれば出来そうなので、試してみました。
(テスト対象がスレッドセーフな場合のみです)
Assertjを使用してますが、Hamcrestでも一緒です。
対象のテストケース
対象のテストケースです。
Thread.sleep()で重い処理を想定してます。
本来は何か処理をしてbooleanを返しますが、簡単のために常にtrueを返すようにしています。
private boolean targetTest(String country, String language) { try { // dummy heavy task Thread.sleep(50L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("test: country=" + country + " lang=" + language); return true; }
Serialにテスト
下記のように複数の組み合わせのパラメータのパターンをテストしようとすると、
組み合わせが膨大になり非常に時間がかかってしまいます。
そしてテストエラーのパターンがあった所で停止してしまい、残りのパターンでの結果は分かりません。
private static final List<String> countries = Arrays.asList( "JP", "IS", "IE", "AZ", "AF", "US", "VI", "AS", "AE", "DZ", "AR", "AW", "AL", "AM", "AI", "AO", "AG", "AD", "YE", "GB", "IO", "VG", "IL", "IT", "IQ", "IR", "IN", "ID", "WF", "UG", "UA", "UZ"); private static final List<String> languages = Arrays.asList( "ar", "an", "hy", "as", "av", "ae", "ay", "az", "ba", "bm", "eu", "be", "bn", "bh", "bi", "bs", "br", "bg", "my", "ca", "ch", "ce", "zh", "zh-CN", "zh-TW", "cu", "cv", "kw", "co", "cr", "cs", "da", "dv", "nl", "dz", "en", "en-US", "en-GB", "en-CA", "en-AU"); @Test public void serialTest() { countries.forEach(country -> { languages.forEach(lang -> { // heavy test assertThat(targetTest(country, lang)).isTrue(); }); }); }
Executorで並行にテスト
Executorで単純に並行にテストすると、エラーとなったケースが検知できません。
@Test public void useExecutorTest() throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(10); List<Callable<Void>> tasks = new ArrayList<>(); try { countries.forEach(country -> { languages.forEach(lang -> { Callable<Void> task = () -> { assertThat(targetTest(country, lang)).isTrue(); return null; }; tasks.add(task); }); }); executorService.invokeAll(tasks); } finally { executorService.shutdown(); } }
並行テスト用メソッドでテスト
下記のようなstaticメソッドを用意。
引数は下記の通り。
- targetTest テスト対象のメソッド
- pairs テスト対象メソッドの引数と戻り値(期待値)のペアのリスト
- threadPoolSize 並行でテストするスレッド数
- maxTimeoutSeconds テスト実行のタイムアウト(時間がかかる場合にfailにするため)
引数の組み合わせを実行していき、期待値と比較していきます。
途中でテストエラーとなったパターンと、例外が発生したパターンをListに保持します。
CountDownLatchを使用して、全パターンのテストが実行したかを確認します。
最終的にエラーや例外が発生してかをチェックしています。
public static <T, U> void testConcurrent(final Function<T, U> targetTest, final List<ArgsExpectedPair<T, U>> pairs, final int threadPoolSize, final int maxTimeoutSeconds) throws InterruptedException { // failed test cases final List<ArgsExpectedPair<T, U>> testFails = Collections.synchronizedList(new ArrayList<>()); // failed with an exception test cases final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<Throwable>()); final ExecutorService threadPool = Executors.newFixedThreadPool(threadPoolSize); try { final CountDownLatch allDone = new CountDownLatch(pairs.size()); for (final ArgsExpectedPair<T, U> pair : pairs) { T args = pair.getArgs(); U expected = pair.getExpected(); Runnable task = () -> { U result = targetTest.apply(args); if (!expected.equals(result)) { testFails.add(pair); } }; threadPool.submit(() -> { try { task.run(); } catch (final Throwable e) { exceptions.add(e); } finally { allDone.countDown(); } }); } assertThat(allDone.await(maxTimeoutSeconds, TimeUnit.SECONDS)) .as(" timeout! More than " + maxTimeoutSeconds + "seconds.").isTrue(); } finally { threadPool.shutdownNow(); } assertThat(testFails).as("test failed.").isEmpty(); assertThat(exceptions).as("failed with exception(s)." + exceptions).isEmpty(); }
引数と結果のペアを保持するためのクラスを用意。
@Data @AllArgsConstructor public static class ArgsExpectedPair<A, B> { private A args; private B expected; }
引数をラップするクラスを用意。
@Data @AllArgsConstructor public static class Locale { private String country; private String language; }
テストするパターンの組み合わせ分、引数と結果のペアのリストを作成して、
対象のテストをFunctionインターフェースでラップします。
引数と結果のペア、対象テスト、スレッド数、タイムアウトを指定してテストを実行。
@Test public void concurrentTest() throws Exception { List<ArgsExpectedPair<Locale, Boolean>> pairs = new ArrayList<>(); countries.forEach(country -> { languages.forEach(lang -> { Locale locale = new Locale(country, lang); pairs.add(new ArgsExpectedPair<>(locale, true)); }); }); Function<Locale, Boolean> targetTestWrapper = (locale) -> { return targetTest(locale.country, locale.getLanguage()); }; testConcurrent(targetTestWrapper, pairs, 10, 10); }
実行結果。
テストがパスします。
test: country=JP lang=ar test: country=JP lang=an test: country=JP lang=ba test: country=JP lang=hy test: country=JP lang=ae test: country=JP lang=ay : : : test: country=UZ lang=dv test: country=UZ lang=dz test: country=UZ lang=en test: country=UZ lang=en-GB test: country=UZ lang=en-CA test: country=UZ lang=en-AU
テストエラーとなるパターンがある場合。
テストがfailとなり、エラーとなったパターンが出力されます。
test: country=JP lang=an test: country=JP lang=ar test: country=JP lang=ba test: country=JP lang=ae test: country=JP lang=av test: country=JP lang=as : : : test: country=UZ lang=nl test: country=UZ lang=en-GB test: country=UZ lang=en-US test: country=UZ lang=en-CA test: country=UZ lang=en-AU test: country=UZ lang=en java.lang.AssertionError: [test failed.] Expecting empty but was:<[MultipleThreadsTest.ArgsExpectedPair(args=MultipleThreadsTest.Locale(country=JP, language=be), expected=true), MultipleThreadsTest.ArgsExpectedPair(args=MultipleThreadsTest.Locale(country=AI, language=bs), expected=true), MultipleThreadsTest.ArgsExpectedPair(args=MultipleThreadsTest.Locale(country=IQ, language=en), expected=true)]>
テストで例外が発生するパターンがある場合。
テストがfailとなり、例外が出力されます。
test: country=JP lang=av test: country=JP lang=ar test: country=JP lang=hy test: country=JP lang=an test: country=JP lang=as test: country=JP lang=az : : : test: country=UZ lang=en test: country=UZ lang=en-CA test: country=UZ lang=en-US test: country=UZ lang=dz test: country=UZ lang=en-GB test: country=UZ lang=en-AU java.lang.AssertionError: [failed with exception(s).[java.lang.RuntimeException]] Expecting empty but was:<[java.lang.RuntimeException]>
指定したタイムアウトをオーバーした場合。
テストがfailとなります。
test: country=JP lang=ar test: country=JP lang=as test: country=JP lang=av test: country=JP lang=hy test: country=JP lang=an test: country=JP lang=az test: country=JP lang=ay test: country=JP lang=bm : : : org.junit.ComparisonFailure: [ timeout! More than 10seconds.] Expected :true Actual :false
終わり。
サンプルはあげておきました。
github.com
参考
https://github.com/junit-team/junit4/wiki/multithreaded-code-and-concurrency