- はじめに
- Gradle の設定
- テストの実行
- アサーション
- Matcher
- ライフサイクル
- Display Names
- @Tag
- Nested Tests
- コンストラクタとテストメソッドへの DI
- Assumptions
- Dynamic Tests
- ライフサイクルコールバック
はじめに
JUnit5 のリリースが近づいています。現在は M2 で M3 の作業が進んでいます。
今のところの予定は以下のようになってます。
- 2016/10/21 M3 リリース
- 2016/11/30 M4 リリース
- 2016/12/30 M5 リリース
JUnit4 とは(中身は)全く別ものです。が普通に使う分には特に今までと同じ感覚で使えます。
Java8 以降をサポートという潔い割り切りになってます。
Version 5.0.0-M2 のユーザガイドからかいつまんでみます。
Gradle の設定
プラグインがあるのでこれを使います。
現在は Gradle プラグインのリポジトリには公開されていないので、buildscript にて依存を追加する必要があります。
buildscript { repositories { jcenter() } dependencies { classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-M2' } } apply plugin: 'org.junit.platform.gradle.plugin'
テストのコンパイルとランタイムの依存を以下のように追加します。
dependencies { testCompile("org.junit.jupiter:junit-jupiter-api:5.0.0-M2") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0-M2") }
太陽系の第5惑星の jupiter をとりあえず入れとけばいいです(少し前は gen5 でしたね)。
上記により依存関係は以下のようになります。
$ ./gradlew dependencies testCompile - Dependencies for source set 'test'. \--- org.junit.jupiter:junit-jupiter-api:5.0.0-M2 +--- org.opentest4j:opentest4j:1.0.0-M1 \--- org.junit.platform:junit-platform-commons:1.0.0-M2 testRuntime - Runtime dependencies for source set 'test'. +--- org.junit.jupiter:junit-jupiter-api:5.0.0-M2 | +--- org.opentest4j:opentest4j:1.0.0-M1 | \--- org.junit.platform:junit-platform-commons:1.0.0-M2 \--- org.junit.jupiter:junit-jupiter-engine:5.0.0-M2 +--- org.junit.platform:junit-platform-engine:1.0.0-M2 | +--- org.junit.platform:junit-platform-commons:1.0.0-M2 | \--- org.opentest4j:opentest4j:1.0.0-M1 \--- org.junit.jupiter:junit-jupiter-api:5.0.0-M2 (*)
opentest4j は、Open Test Alliance for the JVM ということで様々なテストツールやIDEに最低限の共通基盤を提供するものです。中見るとわかりますが、共通で使う例外クラスが幾つか入ってます(JUnit5 チームによるものです)。
build.gradle 全体としては以下のようになります。
buildscript { repositories { jcenter() } dependencies { classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-M2' } } apply plugin: 'java' apply plugin: 'org.junit.platform.gradle.plugin' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' sourceCompatibility = targetCompatibility = '1.8' repositories { jcenter() } dependencies { testCompile("org.junit.jupiter:junit-jupiter-api:5.0.0-M2") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0-M2") }
テストの実行
テストを書いてみましょう。org.junit.jupiter.api.Test
アノテーションを使います。
package example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class JUnit5Example1Test { @Test void firstTest() { assertEquals(3, 1 + 2); } }
JUnit5 ではテストクラスやテストメソッドは public にする必要がなくなりました。
@Test
は interface の default メソッドにも付けることもできます(こちらのissueが実現した)。
junitPlatformTest タスクにてテストが実行されます。 プラグインにより test で実行されます。
$ ./gradlew test ・・・ :junitPlatformTest 10 02, 2016 11:02:21 午後 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines 情報: Discovered TestEngines with IDs: [junit-jupiter] Test run finished after 125 ms [ 1 tests found ] [ 0 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 1 tests successful ] [ 0 tests failed ] [ 0 containers failed]
テスト失敗時には以下のような出力が得られます。
Failures (1): JUnit Jupiter:JUnit5Example1Test:firstTest() JavaMethodSource [javaClass = 'example.JUnit5Example1Test', javaMethodName = 'firstTest', javaMethodParameterTypes = ''] => org.opentest4j.AssertionFailedError: expected: <2> but was: <3> Test run finished after 440 ms [ 1 tests found ] [ 0 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 0 tests successful ] [ 1 tests failed ] [ 0 containers failed]
opentest4j の提供する opentest4j.AssertionFailedError
が出てますね。
ちなみに IntellJ だとこんな感じで expected
が明確になってナイスです。
アサーション
JUnit5 ではアサーションの提供は最低限なもののみ提供されています。
org.junit.jupiter.api.Assertions
に基本的な static メソッドが定義されています。
おなじみの assertEquals
@Test void standardAssertions() { assertEquals(2, 2); assertEquals(4, 4, "The optional assertion message is now the last parameter."); assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- " + "to avoid constructing complex messages unnecessarily."); }
その他もだいたいラムダが渡せるようになっています(遅延評価されるだけであまり実益ないですが)。
アサーションをグルーピングする assertAll
@Test void groupedAssertions() { assertAll("address", () -> assertEquals("John", address.getFirstName()), () -> assertEquals("User", address.getLastName()) ); }
assertAll
ではアサーションが失敗した場合でも、すべてのアサーションが評価されます。
1つのテストメソッドに複数のアサーションを書く場合にはこれを使いましょう ということです。
例外は expectThrows
で捕捉できます。
@Test void exceptionTesting() { Throwable exception = expectThrows(IllegalArgumentException.class, () -> { throw new IllegalArgumentException("a message"); }); assertEquals("a message", exception.getMessage()); }
このあたりはJUnit4から大きく改善されてますね。
まとめると以下のようなものがあります。JUnit4 でもだいたい同じですね。
アサーションメソッド | 説明 |
---|---|
fail | 必ず失敗 |
assertTrue() | trueか |
assertFalse() | falseか |
assertEquals() | 同値(equals)かどうか |
assertNotEquals() | 同値でない(not equals)かどうか |
assertNull() | null かどうか |
assertNotNull() | null でないか |
assertArrayEquals() | 配列の要素が順序通りに同値か |
assertSame() | オブジェクトの参照が同じか |
assertNotSame() | オブジェクトの参照が同じではないか |
assertAll() | 複数のアサーションをまとめる |
assertThrows() | 発生した例外を捕捉して返却する |
Matcher
JUnit5 では Matcher がフレームワークから切り離され、好きな関連ライブラリを使ってねという立ち位置に変わっています。今だと AssertJ ですかね。
build.gradle の依存を以下のように変更します。
dependencies { testCompile 'org.junit.jupiter:junit-jupiter-api:5.0.0-M2' testCompile 'org.assertj:assertj-core:3.5.2' testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.0.0-M2' }
テストはこんな感じでFluentに書けます。
package example; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; class JUnit5Example1Test { @Test void firstTest() { assertThat(2 + 1).isEqualTo(3); assertThat(7).isLessThan(8).isLessThanOrEqualTo(7); } }
もちろん Hamcrest や Google Truth など他のライブラリも普通に使えますので org.junit.jupiter.api.Assertions
をそのまま使うケースは少ないでしょう。
ライフサイクル
アノテーションは変わってますが、見ればわかると思います。
import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.fail; class StandardTests { @BeforeAll static void initAll() { } @BeforeEach void init() { } @Test void succeedingTest() { } @Test void failingTest() { fail("a failing test"); } @Test @Disabled("comment") void skippedTest() { } @AfterEach void tearDown() { } @AfterAll static void tearDownAll() { } }
注意点としては @Ignore
が @Disabled
に変わっているとこぐらいでしょうか。
Display Names
表示名を付けられるようになっています。
package example; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName("テストケースの例") class JUnit5Example1Test { @Test @DisplayName("テストメソッド スペースも含められる") void testWithDisplayNameContainingSpaces() { } }
IDE でこの通り。
@Tag
テストクラスやテストメソッドに @Tag
でタグ付けできます。
package example; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @Tag("model") public class TaggingDemo { @Test @Tag("fast") void testingTaxCalculation() { } @Test @Tag("slow") void testingTaxCalculationSlow() { } }
このようにタグを付け、build.gradle に JUnit5 の設定で対象とするタグを指定します。
junitPlatform { tags { include 'model', 'fast' } }
Gradle JUnit プラグインが対象のタグがついてるテストを実行してくれ、この場合は 'model' により2つのテストが実行されます。
exclude で遅いテストを除外すると、
junitPlatform { tags { include 'model', 'fast' exclude 'slow' } }
@Tag("fast")
が付いてる方だけが実行されます。
自身でアノテーション作ってもよいです。
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Tag; @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Tag("fast") public @interface Fast { }
Nested Tests
@Nested
でテストをネストして構造化できます。
class TestingNestDemo { @Test void outer() { } @Nested class Inner { @Test void testInner() { } } }
コンストラクタとテストメソッドへの DI
JUnit5 ではテストメソッドに引数を取ることができます。
テストメソッドなどに TestInfo
や TestReporter
が指定されていた場合、標準で提供されている TestInfoParameterResolver
や TestReporterParameterResolver
により引数が解決されて DI されます。
TestInfo からはテストの情報を得ることができます。
@Test @DisplayName("TEST 1") @Tag("my tag") void test1(TestInfo testInfo) { assertEquals("TEST 1", testInfo.getDisplayName()); assertTrue(testInfo.getTags().contains("my tag")); }
TestReporter ではテスト結果として任意出力を追加できます。
@Test void reportSingleValue(TestReporter testReporter) { testReporter.publishEntry("a key", "a value"); }
これだけだとあまりうれしくないですが、独自で ParameterResolver
を作ることで真価が発揮されます。
実際にはこちらを見ればよいですが、以下のような ParameterResolver
を実装した Extension
を作成し、
public class MockitoExtension implements TestInstancePostProcessor, ParameterResolver { @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) { MockitoAnnotations.initMocks(testInstance); } @Override public boolean supports(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameterContext.getParameter().isAnnotationPresent(Mock.class); } @Override public Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext) { return getMock(parameterContext.getParameter(), extensionContext); } private Object getMock(Parameter parameter, ExtensionContext extensionContext) { // 省略 モックを返す } }
@ExtendWith
で指定すれば任意オブジェクトがDIできるようになります。
@ExtendWith(MockitoExtension.class) class MyMockitoTest { @BeforeEach void init(@Mock Person person) { when(person.getName()).thenReturn("Dilbert"); } @Test void simpleTestWithInjectedMock(@Mock Person person) { assertEquals("Dilbert", person.getName()); } }
@ExtendWith
で複数の拡張を盛り込むときは、横に並べても
@ExtendWith({ FooExtension.class, BarExtension.class }) class MyTest { }
縦にならべても大丈夫です。
@ExtendWith(FooExtension.class) @ExtendWith(BarExtension.class) class MyTest { }
Assumptions
org.junit.jupiter.Assumptions
に static メソッドでまとめられています。JUnit4とだいたい同じです。
テストの前提条件を記述します。
@Test void testOnlyOnCiServer() { assumeTrue("CI".equals(System.getenv("ENV"))); // CIサーバの場合以降のテストを実行 } @Test void testInAllEnvironments() { assumingThat("CI".equals(System.getenv("ENV")), () -> { // CI サーバでのみ実行 assertEquals(2, 2); }); // 全ての環境で実行 assertEquals("a string", "a string"); }
注意点はラムダを渡せるようになった点ぐらいです。
Dynamic Tests
@TestFactory
アノテーションを付けたメソッドから org.junit.jupiter.api.DynamicTest
リストやストリーム返すようにすることで、動的に作成したテストができます。
@TestFactory Iterable<DynamicTest> dynamicTestsFromIterable() { return Arrays.asList( dynamicTest("dynamic test 1", () -> assertTrue(true)), dynamicTest("dynamic test 2", () -> assertEquals(4, 2 * 2)) ); }
dynamicTest()
にラムダ渡して DynamicTest
を作成します。
Stream
で返したり。
@TestFactory Stream<DynamicTest> dynamicTestsFromIntStream() { return IntStream.iterate(0, n -> n + 2).limit(10).mapToObj( n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); }
多くの入力値に対してテストする場合や外部ファイルから動的にテスト作りたいときなどに使うようですが、普段使いはあまりないかもしれません。
ライフサイクルコールバック
普段使いはしないと思いますが、テスト実行時のライフサイクルで以下のコールバックが定義されています。
BeforeAllCallback
BeforeEachCallback
BeforeTestExecutionCallback
AfterTestExecutionCallback
AfterEachCallback
AfterAllCallback
こんな感じでコールバックを受ける Extension を用意して、
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final Logger LOG = Logger.getLogger(TimingExtension.class.getName()); @Override public void beforeTestExecution(TestExtensionContext context) throws Exception { getStore(context).put(context.getTestMethod().get(), System.currentTimeMillis()); } @Override public void afterTestExecution(TestExtensionContext context) throws Exception { Method testMethod = context.getTestMethod().get(); long start = getStore(context).remove(testMethod, long.class); long duration = System.currentTimeMillis() - start; LOG.info(() -> String.format("Method [%s] took %s ms.", testMethod.getName(), duration)); } private Store getStore(TestExtensionContext context) { return context.getStore(Namespace.create(getClass(), context)); } }
@ExtendWith
で指定すれば、
@ExtendWith(TimingExtension.class) class TimingExtensionTests { @Test void sleep20ms() throws Exception { Thread.sleep(20); } }
INFO: Method [sleep20ms] took 24 ms.
のように経過時間をログ出力したりといったことができます。
あとは TestExecutionExceptionHandler
で Extension つくれたり。
public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler { @Override public void handleTestExecutionException(TestExtensionContext context, Throwable throwable) throws Throwable { if (throwable instanceof IOException) { return; } throw throwable; } }
以上駆け足で見てきました。今までと同じような感じで使っていけそうですね。