技術評論社 WEB+DB PRESS Vol. 109 特集2 “[実践] Kotlin” (第2, 3, 5章) を執筆しました。
続きを読む 技術評論社 WEB+DB PRESS Vol. 109 特集2 を執筆しました「JUnit」タグアーカイブ
JUnit 5 ユーザーズガイドを読む 3
JUnit 5 のユーザーズガイドを読みました。 公式ドキュメントのうち、重要そうなところをピックアップしてまとめています。 そのため公式ドキュメントにあってこちらにない文章もあります。 またその逆もあります。
この記事は JUnit 5 ユーザーズガイドを読む 2 からの続きです。
3 テストを書く
3.4 アサーション
3.4.1 サードパーティのアサーションライブラリ
JUnit Jupiter が提供するアサーションは多くのテストシナリオをカバーしますが、 より強力で付加機能を持った matchers のようなものが必要もしくは好ましい場合があります。 そのようなケースでは、 JUnit チーム は AssertJ, Hamcrast, Truth などの サードパーティアのサーションライブラリの使用を勧めます。 そのため開発者は自由にアサーションライブラリを選んで使うことができます。
たとえば、 matchers
と流れるようなAPIとの組み合わせにより、 アサーションをより表現豊かに読みやすくすることができます。 しかしながら、 JUnit Jupiter の org.junit.jupiter.api.Assertions
クラス は、 JUnit 4 で org.junit.Assert
クラス にあるような、 Hamcrest の Matcher
を受け付ける assertThat()
メソッドを提供していません。 代わりに、 開発者はサードパーティのアサーションライブラリのための、ビルトインサポートを利用することが推奨されています。
次の例は JUnit Jupiter テスト で Hamcrest から assertThat()
を使うサンプルです。 Hamcrast ライブラリ が クラスパスに追加されていれば、 assertThat()
や is()
, equalTo()
といったアサーションを静的にインポートすることができ、 テストの中で下のように assertWithHamcrestMatcher()
のように使用できます。
1 2 3 |
dependencies { testImplementation("org.hamcrest:hamcrest:2.1") } |
1 2 3 4 5 6 7 8 9 10 11 12 |
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Test internal class HamcrestAssertionDemo { @Test fun assertWithHamcrestMatcher() { assertThat(2 + 1, `is`(equalTo(3))) } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import org.junit.jupiter.api.Test; class HamcrestAssertionDemo { @Test void assertWithHamcrestMatcher() { assertThat(2 + 1, is(equalTo(3))); } } |
JUnit 4 のプログラミングモデルで作成されたレガシーなテストは、 org.junitAssert#assertThat
を使い続けることができます。
3.4 アサンプション
アサンプションは、テストコードを実行する前に状況を確認し、テストを継続しても意味がない場合に テストを Abort させたり、 状況に応じてテスト内容を変更する機能です。 JUnit Jupiter は JUnit 4 が提供しているアサンプションメソッドのサブセットといくつかの Java 8 のラムダでうまく使えるメソッドです。 すべての JUnit Jupiter の アサンプションは org.junit.jupiter.Assumptions
クラス の静的メソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Assumptions.assumingThat import org.junit.jupiter.api.Test internal class AssumptionsDemo { @Test fun `test only on CI server`() { assumeTrue("CI" == System.getenv("ENV")) // remainder of test } @Test fun `test only on developer workstation`() { assumeTrue( "DEV" == System.getenv("ENV") ) { "Aborting test: not on developer workstation" } // remainder of test } @Test fun `test in all environments`() { assumingThat( "CI" == System.getenv("ENV") ) { // perform these assertions only on the CI server assertEquals(2, 2) } // perform these assertions in all environments assertEquals("a string", "a string") } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.Assumptions.assumingThat; import org.junit.jupiter.api.Test; class AssumptionsDemo { @Test void testOnlyOnCiServer() { assumeTrue("CI".equals(System.getenv("ENV"))); // remainder of test } @Test void testOnlyOnDeveloperWorkstation() { assumeTrue("DEV".equals(System.getenv("ENV")), () -> "Aborting test: not on developer workstation"); // remainder of test } @Test void testInAllEnvironments() { assumingThat("CI".equals(System.getenv("ENV")), () -> { // perform these assertions // only on the CI server assertEquals(2, 2); }); // perform these assertions in all environments assertEquals("a string", "a string"); } } |
3.6 テストの無効化
@Disabled
アノテーション または 条件付きテスト実行の項で議論するアノテーション、 カスタム ExecutionCondition を用いて テストクラス全体またはそれぞれのテストメソッドを無効化できます。
ここでは @Disabled
をテストクラスに使ったコードを紹介します。
1 2 3 4 5 6 7 8 9 |
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @Disabled internal class DisabledClassDemo { @Test fun `test will be skipped`() { } } |
1 2 3 4 5 6 7 8 9 |
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @Disabled class DisabledClassDemo { @Test void testWillBeSkipped() { } } |
@Disabled
をテストメソッドに使うと次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test internal class DisabledTestsDemo { @Disabled @Test fun `test will be skipped`() { } @Test fun `test will be executed`() { } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class DisabledTestsDemo { @Disabled @Test void testWillBeSkipped() { } @Test void testWillBeExecuted() { } } |
3.7 条件付きテスト実行
JUnit Jupiter の ExecutionCondition
エクステンション API を用いると ある条件に基づいたコンテナ及びテストを コードを用いて 有効 または DisabledCondition
で、 @Disabled
アノテーションをサポートします。 @Disabled
に加え、 JUnit Jupiter はほかにもいくつかのアノテーションに基づいた条件をサポートしています。 それらの条件は org.junit.jupiter.api.condition
パッケージ にあり、 開発者はコンテナ及びテストを宣言的に(アノテーションを用いて)有効・無効にできます。
- 合成アノテーション
- 以下に記載されているすべての条件アノテーションは、独自の合成アノテーションを作るためのメタアノテーションとしても使用できます。 例えば、
@EnabledOnOs
デモ にある@TestOnMac
アノテーション は@Test
と@EnabledOnOs
を単一の、再利用できるアノテーションに合成したものです。
下記セクションのそれぞれの条件アノテーションは、テストインターフェース、テストクラス、テストメソッドに一度だけ宣言できます。 もし条件アノテーションがある要素に直接的または間接的、メタ的に複数回使われている場合、 JUnit によって発見された最初のアノテーションのみが使われます。 追加のアノテーションは無視されます。 それぞれの条件アノテーションは他の条件アノテーションと複合的に使用可能です。
3.7.1 オペレーティングシステム条件
コンテナとテストは @EnableOnOs
アノテーション, @DisabledOnOs
アノテーション を用いて 特定のオペレーティングシステム上で有効化・無効化できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.DisabledOnOs import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.Test @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @Test @EnabledOnOs(OS.MAC) internal annotation class TestOnMac internal class OsTest { @Test @EnabledOnOs(OS.MAC) fun `only on macOS`() { // ... } @TestOnMac fun `test on Mac`() { // ... } @Test @EnabledOnOs(OS.LINUX, OS.MAC) fun `on Linux or Mac`() { // ... } @Test @DisabledOnOs(OS.WINDOWS) fun `not on Windows`() { // ... } } |
Java のサンプルコードは公式にあったものをそのまま掲載します。 クラスになっていないのでそのままでは動かないと思うのですが。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Test @EnabledOnOs(MAC) void onlyOnMacOs() { // ... } @TestOnMac void testOnMac() { // ... } @Test @EnabledOnOs({ LINUX, MAC }) void onLinuxOrMac() { // ... } @Test @DisabledOnOs(WINDOWS) void notOnWindows() { // ... } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Test @EnabledOnOs(MAC) @interface TestOnMac { } |
3.7.2 Java 実行環境条件
コンテナとテストは、 @EnableOnJre
アノテーション、 @DisabledOnJre
アノテーション を用いて、 特定の JRE 環境において有効化・無効化できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import org.junit.jupiter.api.condition.DisabledOnJre import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledOnJre import org.junit.jupiter.api.condition.JRE internal class JreTest { @Test @EnabledOnJre(JRE.JAVA_8) fun `only on Java 8`() { // ... } @Test @EnabledOnJre(JRE.JAVA_9, JRE.JAVA_10) fun `on Java 9 or 10`() { // ... } @Test @DisabledOnJre(JRE.JAVA_9) fun `not on Java 9`() { // ... } } |
Java のサンプルコードは公式にあったものをそのまま掲載します。 クラスになっていないのでそのままでは動かないと思うのですが。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Test @EnabledOnJre(JAVA_8) void onlyOnJava8() { // ... } @Test @EnabledOnJre({ JAVA_9, JAVA_10 }) void onJava9Or10() { // ... } @Test @DisabledOnJre(JAVA_9) void notOnJava9() { // ... } |
3.7.3 システムプロパティ条件
コンテナとテストは、 @EnabledIfEnvironmentVariable
アノテーション、 @DisabledIfEnvironmentVariable
アノテーション の named
属性に指定された環境変数に応じて有効化・無効化できます。 matches
属性 に渡された値は正規表現として扱われます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable import org.junit.jupiter.api.Test class EnvironmentVariableTest { @Test @EnabledIfEnvironmentVariable( named = "ENV", matches = "staging-server" ) fun onlyOnStagingServer() { // ... } @Test @DisabledIfEnvironmentVariable( named = "ENV", matches = ".*development.*" ) fun notOnDeveloperWorkstation() { // ... } } |
Java のサンプルコードは公式にあったものをそのまま掲載します。 クラスになっていないのでそのままでは動かないと思うのですが。
1 2 3 4 5 6 7 8 9 10 11 |
@Test @EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server") void onlyOnStagingServer() { // ... } @Test @DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*") void notOnDeveloperWorkstation() { // ... } |
3.4.4 スクリプトベース条件
JUnit Jupiter では コンテナとテストを @EnabledIf
アノテーション、 @DisabledIf
アノテーション を使い、 スクリプトの評価値に応じて有効化・無効化できます。 スクリプトは JavaScript, Groovy をはじめその他の JSR 223 で定義された JavaScript API をサポートそいているスクリプト言語で記述できます。
@EnabledIf
アノテーション、 @DisabledIf
アノテーション を使ったテストの実行は試験的機能です。 試験的機能について、詳しくは試験的APIの表を参照してください。
もし、記述したスクリプトがOS や JRE バージョン、 特定の JVM システム プロパティ、 特定の環境変数などに依存する場合は、 他のアノテーションの使用を検討したほうがいいです。
もし同じスクリプトベースの条件を複数回使用する場合は、 後述する ExecutionCondition
拡張 の記述を検討しましょう。 実行が速くなり、タイプセーフで、メンテナンスしやすくなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import java.time.LocalDate import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.condition.DisabledIf import org.junit.jupiter.api.condition.EnabledIf import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.Test class ScriptBasedTest { @Test // Static JavaScript expression. @EnabledIf("2 * 3 == 6") fun `will be executed`() { // ... } @RepeatedTest(10) // Dynamic JavaScript expression. @DisabledIf("Math.random() < 0.314159") fun `might not be executed`() { // ... } @Test // Regular expression testing bound system property. @DisabledIf("/32/.test(systemProperty.get('os.arch'))") fun `disabled on 32 bit architectures`() { assertFalse(System.getProperty("os.arch").contains("32")) } @Test @EnabledIf("'CI' == systemEnvironment.get('ENV')") fun `only on CI server`() { assertTrue("CI" == System.getenv("ENV")) } @Test // Multi-line script, custom engine name and custom reason. @EnabledIf( value = *arrayOf( "load('nashorn:mozilla_compat.js')", "importPackage(java.time)", "", "var today = LocalDate.now()", "var tomorrow = today.plusDays(1)", "tomorrow.isAfter(today)" ), engine = "nashorn", reason = "Self-fulfilling: {result}" ) fun `the day after tomorrow`() { val today = LocalDate.now() val tomorrow = today.plusDays(1) assertTrue(tomorrow.isAfter(today)) } } |
Java のサンプルコードは公式にあったものをそのまま掲載します。 クラスになっていないのでそのままでは動かないと思うのですが。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
@Test // Static JavaScript expression. @EnabledIf("2 * 3 == 6") void willBeExecuted() { // ... } @RepeatedTest(10) // Dynamic JavaScript expression. @DisabledIf("Math.random() < 0.314159") void mightNotBeExecuted() { // ... } @Test // Regular expression testing bound system property. @DisabledIf("/32/.test(systemProperty.get('os.arch'))") void disabledOn32BitArchitectures() { assertFalse(System.getProperty("os.arch").contains("32")); } @Test @EnabledIf("'CI' == systemEnvironment.get('ENV')") void onlyOnCiServer() { assertTrue("CI".equals(System.getenv("ENV"))); } @Test // Multi-line script, custom engine name and custom reason. @EnabledIf(value = { "load('nashorn:mozilla_compat.js')", "importPackage(java.time)", "", "var today = LocalDate.now()", "var tomorrow = today.plusDays(1)", "tomorrow.isAfter(today)" }, engine = "nashorn", reason = "Self-fulfilling: {result}") void theDayAfterTomorrow() { LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plusDays(1); assertTrue(tomorrow.isAfter(today)); } |
スクリプト バインディング
次の名前は、各スクリプトコンテキストでバインドされているため、スクリプト内で使用可能です。 アクセサは、単純な String get(String name)
メソッドで、 マップのようなアクセスを実現しています。
名前 | 型 | 説明 |
---|---|---|
systemEnvironment |
アクセサ | OSの環境変数へのアクセサ。 |
systemProperty |
JVM のシステムプロパティへのアクセサ。 | |
junitConfigurationParameter |
設定パラメータへのアクセサ。 | |
junitDisplayName |
String |
テストおよびコンテナの表示名。 |
junitTags |
Set<String> |
テストまたはコンテナに付与されたタグ名。 |
junitUniqueId |
String |
テストまたはコンテナの一意のID。 |
JUnit 5 ユーザーズガイドを読む 2
JUnit 5 のユーザーズガイドを読みました。 公式ドキュメントのうち、重要そうなところをピックアップしてまとめています。 そのため公式ドキュメントにあってこちらにない文章もあります。
この記事は からの続きです。
続きを読む JUnit 5 ユーザーズガイドを読む 2JUnit 5 ユーザーズガイド を読む 1
JUnit 5 のユーザーズガイドを読みました。 公式ドキュメントのうち、重要そうなところをピックアップしてまとめています。 そのため公式ドキュメントにあってこちらにない文章もあります。
続きを読む JUnit 5 ユーザーズガイド を読む 1テスト駆動開発で使う テストフレームワークの仕組み
参考書籍は Test-Driven Development By Example、 日本語版は テスト駆動開発入門です。
Money Example の次は jUnit のようなテストツールを作ってみるお話です。 30以上の言語で使われている xUnit は、 今や巨大な資産ですが、 そういったものは自分で作れるものなんでしょうか。
テストフレームワークの仕組み
読んでみると、実際にやっていることは難しくなさそうです。 肝となるのは 一連のテストの結果 (実行・成功・失敗したテスト数) をどうやって保持するかではないかと思いました。 独自にテストフレームワークを作るときに役に立ちそうです。
社内でも独自にテストフレームワークを作っている人がいます、 かなりシンプルにできるそうです。 その人によると、 PHPUnit などのテストフレームワークは いろんなファンクションや機能をつけているから複雑になっているそうです。 そうじゃないと使ってもらえないからだとも言っていましたが。
JUnit や PHPUnit は TestCase
だったかのクラスを継承してテストコードを書いていくので 今回説明されていたやり方と同じですね。 でも rspec だと describe で書き始めたりするし クラス の 定義 で始めるわけでもないので 本書とは別のアプローチかもしれません。