- 投稿日:2019-02-27T11:55:35+09:00
Kotlin で JUnit 5
Kotlin で JUnit 5 を使い始めました。両者を組み合わせる事例はあまり見かけませんでしたが、よい感触を得ましたので所感を残しておこうと思います。
背景
フルスクラッチで API サーバを書く案件が始まり、トレンドに乗って Kotlin で実装することになりました。テストフレームワークは使い慣れた JUnit を採用しましたが、せっかくの機会ですので JUnit も最新 5 系に挑戦することにしました。
本記事とは直接関係ありませんが、前提とするプロジェクトは以下の通りです。
- 全員が Java の API サーバ開発経験あり
- Kotlin 経験はまちまち
- 実装・テストともにすべて Kotlin
- サーバのフレームワークは Spring
良かったこと: モダンなテストが書ける
JUnit 4 から大きく機能拡張されたことと、Kotlin による簡素な記述のおかげで、現代的で保守しやすいテストが書けると感じました。特に、以下が容易に実現できる点はありがたいです。
- ビヘイビア駆動開発 (BDD)
- Parameterized Test
非 static な内部クラスでネストできる
JUnit 4 でも static な内部クラスでネストすることはできましたが、JUnit 5 では
@Nested
アノテーションを使うことで、static でない内部クラスをネストすることができるようになりました。この機能は 前提条件を共通化できる という意味で大変気に入っています。たとえば以下のようにクラス名とフィクスチャを対応させることで、BDD のようなテスト記述ができるようになります。interface Subscriber { fun receive(message: String): Boolean } class Publisher(private val subscriber: Subscriber) { fun send(message: String) = subscriber.receive(message) } internal class PublisherTest { private lateinit var publisher: Publisher @Mock private lateinit var subscriber: Subscriber @BeforeEach fun setUp() { MockitoAnnotations.initMocks(this) publisher = Publisher(subscriber) } @Nested inner class WhenSubscriberCannotReceive { // "Subscriber cannot receive" という状態を記述できる. @BeforeEach fun setUp() { whenever(subscriber.receive(any())).thenReturn(false) } @Test fun thenThenFailsToSend() { val actual = publisher.send("bye") assertThat(actual, equalTo(false)) verify(subscriber).receive("bye") } } @Nested inner class WhenSubscriberReceives { // "Subscriber receives" という状態を記述できる. @BeforeEach fun setUp() { whenever(subscriber.receive(any())).thenReturn(true) } @Test fun thenSucceedsToSend() { val actual = publisher.send("hello") assertThat(actual, equalTo(true)) verify(subscriber).receive("hello") } } }初歩的ですが、Kotlin で書く際は、Java と異なり内部クラスがデフォルトで static であるため、
inner
キーワードを忘れないように注意が必要です。テストに関連するオブジェクトのスコープが柔軟に変更できるので、ヘルパーメソッドによる共通化がしやすいと感じました。Kotlin の簡潔な記述とも相性がよさそうです。
internal class FooTest { private lateinit var publisher: Publisher @Mock private lateinit var subscriber: Subscriber @BeforeEach fun setUp() { MockitoAnnotations.initMocks(this) publisher = Publisher(subscriber) } @Nested inner class WhenSubscriberCannotReceive { @BeforeEach fun setUp() = stubSubscriber(canReceive = false) @Test fun thenThenFailsToSend() = assertThat(testSending("bye"), equalTo(false)) } @Nested inner class WhenSubscriberReceives { @BeforeEach fun setUp() = stubSubscriber(canReceive = true) @Test fun thenSucceedsToSend() = assertThat(testSending("hello"), equalTo(true)) } // ヘルパーメソッドでテストを共通化する. // ここから `publisher` や `subscriber` が見えているところがポイント. private fun stubSubscriber(canReceive: Boolean) { whenever(subscriber.receive(any())).thenReturn(canReceive) } private fun testSending(message: String): Boolean { val succeeded = publisher.send(message) verify(subscriber).receive(message) return succeeded } }Parameterized テストがとても書きやすい
Parameterized テストは、テストケースとテストロジックを分離できる手法で、特に多くの組み合わせの検証が必要なケースでは大変強力です。JUnit 4 の parameterized テストは、おまじないがエグい ためかなり苦労させられたのですが、JUnit 5 では極めて直感的に記述できるようになりました。
簡単なテストケースであればアノテーションだけでも記述できるのですが、実用上は
@MethodSource
と@ArgumentsSource
の 2 つが、手軽さと柔軟さのバランスがよいと感じています。
@MethodSource
は static メソッドに記述するだけで本当にお手軽に利用できます。Java の static メソッドを参照するので、Kotlin で書く際は companion object に関数を定義して@JvmStatic
を付与します。internal class AddTest { @ParameterizedTest @MethodSource("testCases") fun addTest(lhs: Int, rhs: Int, expected: Int) { val actual = lhs + rhs assertThat(actual, equalTo(expected)) } companion object { @Suppress("unused") // used by `addTest` @JvmStatic fun testCase() = listOf( arguments(0, 0, 0), arguments(1, 1, 2), arguments(1, -1, 0) ) } }細かいですが、Kotlin だと Java で書くよりもテストケースを並べている感があって、相性がよさそうだと感じました。
@MethodSource
もあまり不満はないのですが、気になる点を挙げるならば以下の通りです。いずれも気にしすぎかもしれませんが。
- IntelliJ が (執筆時点では) メソッド不使用を警告してくる
- いちいち抑制するのが面倒
- 正しく解決されるかどうか実行されるまで分からない
- IDE の補完が効かない
@JvmStatic
のメタ情報が気持ち悪い
- Kotlin なのに!
これらの問題はすべて
@ArgumentsSource
で解決できます。internal class AddTest { @ParameterizedTest @ArgumentsSource(TestCase::class) fun addTest(lhs: Int, rhs: Int, expected: Int) { val actual = lhs + rhs assertThat(actual, equalTo(expected)) } private class TestCase : ArgumentsProvider { override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of( arguments(0, 0, 0), arguments(1, 1, 2), arguments(1, -1, 0) ) } }強いて気になるのは、コード量が少しだけ増えることと、Kotlin なのに
Stream
を強要されることでしょうか。これもご愛嬌のレベルだと思います。例外テストが書きやすい
JUnit 5 から
assertThrows
が採用され、格段に例外テストが書きやすくなりました。Java でも重宝しますが、なんと Kotlin 向けのassertThrows
が提供されており、大変気持ちよく記述できます。val error = assertThrows<IOException> { process() } assertThat(error.message, equalTo("expected message"))Java 用とパッケージを間違えないようにだけ注意が必要です。Kotlin 用は
org.junit.jupiter.api.assertThrows
です。Kotlin に助けられたこと
JUnit 5 と Kotlin を組み合わせで困ることはほとんどなかったのですが、現実的には JUnit の関連ライブラリの利用において困ったこともありました。これらは別の記事で取り扱おうと思いますが、一方で Kotlin のおかげで偶然解決した問題もありましたので紹介します。
PowerMock の代わりに MockK が使える
Java では主に static メソッドのモンキーパッチとして活躍していた PowerMock ですが、残念ながら本記事執筆時点では JUnit 5 をサポートしていないようです (案件自体はあるようですが)。本プロジェクトはフルスクラッチの開発であり、かつ DI が強力な Spring を採用しましたので、基本的には PowerMock を使いたくなる状況自体を回避する方針をとっています。
しかし、以下のような例外的状況も発生しました。
- Kotlin なので、純粋関数や拡張関数を使いたいケースももちろんある
- サードパーティーのライブラリで static メソッドを利用せざるを得ないケースはどうしようもない
これらを解決してくれるライブラリとして、MockK を採用しました。使用感は Python の patch に似ていて、すぐ慣れることができました。以下の例は拡張関数にモックを適用していますが、純粋関数や Java の static メソッドにも同じようにモックを適用できます。
// Function.kt fun String.extension() = "orange" // FunctionTest.kt internal class FunctionTest { @Test fun mockedSuccessfully() { assertThat("apple".extension(), equalTo("banana")) } companion object { private const val MOCKED_PACKAGE = "mocked.FunctionKt" @Suppress("unused") // IntelliJ IDEA warns... @JvmStatic @BeforeAll fun initialize() { mockkStatic(MOCKED_PACKAGE) every { "apple".extension() } returns "banana" } @Suppress("unused") // IntelliJ IDEA warns... @JvmStatic @AfterAll fun finalize() { unmockkStatic(MOCKED_PACKAGE) } } }MockK はとても多機能なため全容を研究しきれていませんが、モックライブラリの機能に頼りすぎて設計が疎かになっては元も子もありませんので、どうしようもないときだけ MockK で実現できないか調査する、という方針をとっています。
まとめ
JUnit 5 はモダンなテストが書けて大変使いやすく、Kotlin との相性もよさそうでした。MockK という思わぬメリットを享受できた点も幸運でした。両者の組み合わせについて大きなデメリットも感じませんでしたので、今後も活用していきたいと思います。
参考
- 投稿日:2019-02-27T11:55:35+09:00
Kotlin で JUnit 5 を使ってみた
Kotlin で JUnit 5 を使い始めました。両者を組み合わせる事例はあまり見かけませんでしたが、よい感触を得ましたので所感を残しておこうと思います。
背景
フルスクラッチで API サーバを書く案件が始まり、トレンドに乗って Kotlin で実装することになりました。テストフレームワークは使い慣れた JUnit を採用しましたが、せっかくの機会ですので JUnit も最新 5 系に挑戦することにしました。
本記事とは直接関係ありませんが、前提とするプロジェクトは以下の通りです。
- 全員が Java の API サーバ開発経験あり
- Kotlin 経験はまちまち
- 実装・テストともにすべて Kotlin
- サーバのフレームワークは Spring
良かったこと: モダンなテストが書ける
JUnit 4 から大きく機能拡張されたことと、Kotlin による簡素な記述のおかげで、現代的で保守しやすいテストが書けると感じました。特に、以下が容易に実現できる点はありがたいです。
- ビヘイビア駆動開発 (BDD)
- Parameterized Test
非 static な内部クラスでネストできる
JUnit 4 でも static な内部クラスでネストすることはできましたが、JUnit 5 では
@Nested
アノテーションを使うことで、static でない内部クラスをネストすることができるようになりました。この機能は 前提条件を共通化できる という意味で大変気に入っています。たとえば以下のようにクラス名とフィクスチャを対応させることで、BDD のようなテスト記述ができるようになります。interface Subscriber { fun receive(message: String): Boolean } class Publisher(private val subscriber: Subscriber) { fun send(message: String) = subscriber.receive(message) } internal class PublisherTest { private lateinit var publisher: Publisher @Mock private lateinit var subscriber: Subscriber @BeforeEach fun setUp() { MockitoAnnotations.initMocks(this) publisher = Publisher(subscriber) } @Nested inner class WhenSubscriberCannotReceive { // "Subscriber cannot receive" という状態を記述できる. @BeforeEach fun setUp() { whenever(subscriber.receive(any())).thenReturn(false) } @Test fun thenThenFailsToSend() { val actual = publisher.send("bye") assertThat(actual, equalTo(false)) verify(subscriber).receive("bye") } } @Nested inner class WhenSubscriberReceives { // "Subscriber receives" という状態を記述できる. @BeforeEach fun setUp() { whenever(subscriber.receive(any())).thenReturn(true) } @Test fun thenSucceedsToSend() { val actual = publisher.send("hello") assertThat(actual, equalTo(true)) verify(subscriber).receive("hello") } } }初歩的ですが、Kotlin で書く際は、Java と異なり内部クラスがデフォルトで static であるため、
inner
キーワードを忘れないように注意が必要です。テストに関連するオブジェクトのスコープが柔軟に変更できるので、ヘルパーメソッドによる共通化がしやすいと感じました。Kotlin の簡潔な記述とも相性がよさそうです。
internal class FooTest { private lateinit var publisher: Publisher @Mock private lateinit var subscriber: Subscriber @BeforeEach fun setUp() { MockitoAnnotations.initMocks(this) publisher = Publisher(subscriber) } @Nested inner class WhenSubscriberCannotReceive { @BeforeEach fun setUp() = stubSubscriber(canReceive = false) @Test fun thenThenFailsToSend() = assertThat(testSending("bye"), equalTo(false)) } @Nested inner class WhenSubscriberReceives { @BeforeEach fun setUp() = stubSubscriber(canReceive = true) @Test fun thenSucceedsToSend() = assertThat(testSending("hello"), equalTo(true)) } // ヘルパーメソッドでテストを共通化する. // ここから `publisher` や `subscriber` が見えているところがポイント. private fun stubSubscriber(canReceive: Boolean) { whenever(subscriber.receive(any())).thenReturn(canReceive) } private fun testSending(message: String): Boolean { val succeeded = publisher.send(message) verify(subscriber).receive(message) return succeeded } }Parameterized テストがとても書きやすい
Parameterized テストは、テストケースとテストロジックを分離できる手法で、特に多くの組み合わせの検証が必要なケースでは大変強力です。JUnit 4 の parameterized テストは、おまじないがエグい ためかなり苦労させられたのですが、JUnit 5 では極めて直感的に記述できるようになりました。
簡単なテストケースであればアノテーションだけでも記述できるのですが、実用上は
@MethodSource
と@ArgumentsSource
の 2 つが、手軽さと柔軟さのバランスがよいと感じています。
@MethodSource
は static メソッドに記述するだけで本当にお手軽に利用できます。Java の static メソッドを参照するので、Kotlin で書く際は companion object に関数を定義して@JvmStatic
を付与します。internal class AddTest { @ParameterizedTest @MethodSource("testCases") fun addTest(lhs: Int, rhs: Int, expected: Int) { val actual = lhs + rhs assertThat(actual, equalTo(expected)) } companion object { @Suppress("unused") // used by `addTest` @JvmStatic fun testCase() = listOf( arguments(0, 0, 0), arguments(1, 1, 2), arguments(1, -1, 0) ) } }細かいですが、Kotlin だと Java で書くよりもテストケースを並べている感があって、相性がよさそうだと感じました。
@MethodSource
もあまり不満はないのですが、気になる点を挙げるならば以下の通りです。いずれも気にしすぎかもしれませんが。
- IntelliJ が (執筆時点では) メソッド不使用を警告してくる
- いちいち抑制するのが面倒
- 正しく解決されるかどうか実行されるまで分からない
- IDE の補完が効かない
@JvmStatic
のメタ情報が気持ち悪い
- Kotlin なのに!
これらの問題はすべて
@ArgumentsSource
で解決できます。internal class AddTest { @ParameterizedTest @ArgumentsSource(TestCase::class) fun addTest(lhs: Int, rhs: Int, expected: Int) { val actual = lhs + rhs assertThat(actual, equalTo(expected)) } private class TestCase : ArgumentsProvider { override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of( arguments(0, 0, 0), arguments(1, 1, 2), arguments(1, -1, 0) ) } }強いて気になるのは、コード量が少しだけ増えることと、Kotlin なのに
Stream
を強要されることでしょうか。これもご愛嬌のレベルだと思います。例外テストが書きやすい
JUnit 5 から
assertThrows
が採用され、格段に例外テストが書きやすくなりました。Java でも重宝しますが、なんと Kotlin 向けのassertThrows
が提供されており、大変気持ちよく記述できます。val error = assertThrows<IOException> { process() } assertThat(error.message, equalTo("expected message"))Java 用とパッケージを間違えないようにだけ注意が必要です。Kotlin 用は
org.junit.jupiter.api.assertThrows
です。Kotlin に助けられたこと
JUnit 5 と Kotlin を組み合わせで困ることはほとんどなかったのですが、現実的には JUnit の関連ライブラリの利用において困ったこともありました。これらは別の記事で取り扱おうと思いますが、一方で Kotlin のおかげで偶然解決した問題もありましたので紹介します。
PowerMock の代わりに MockK が使える
Java では主に static メソッドのモンキーパッチとして活躍していた PowerMock ですが、残念ながら本記事執筆時点では JUnit 5 をサポートしていないようです (案件自体はあるようですが)。本プロジェクトはフルスクラッチの開発であり、かつ DI が強力な Spring を採用しましたので、基本的には PowerMock を使いたくなる状況自体を回避する方針をとっています。
しかし、以下のような例外的状況も発生しました。
- Kotlin なので、純粋関数や拡張関数を使いたいケースももちろんある
- サードパーティーのライブラリで static メソッドを利用せざるを得ないケースはどうしようもない
これらを解決してくれるライブラリとして、MockK を採用しました。使用感は Python の patch に似ていて、すぐ慣れることができました。以下の例は拡張関数にモックを適用していますが、純粋関数や Java の static メソッドにも同じようにモックを適用できます。
// Function.kt fun String.extension() = "orange" // FunctionTest.kt internal class FunctionTest { @Test fun mockedSuccessfully() { assertThat("apple".extension(), equalTo("banana")) } companion object { private const val MOCKED_PACKAGE = "mocked.FunctionKt" @Suppress("unused") // IntelliJ IDEA warns... @JvmStatic @BeforeAll fun initialize() { mockkStatic(MOCKED_PACKAGE) every { "apple".extension() } returns "banana" } @Suppress("unused") // IntelliJ IDEA warns... @JvmStatic @AfterAll fun finalize() { unmockkStatic(MOCKED_PACKAGE) } } }MockK はとても多機能なため全容を研究しきれていませんが、モックライブラリの機能に頼りすぎて設計が疎かになっては元も子もありませんので、どうしようもないときだけ MockK で実現できないか調査する、という方針をとっています。
まとめ
JUnit 5 はモダンなテストが書けて大変使いやすく、Kotlin との相性もよさそうでした。MockK という思わぬメリットを享受できた点も幸運でした。両者の組み合わせについて大きなデメリットも感じませんでしたので、今後も活用していきたいと思います。
参考
- 投稿日:2019-02-27T01:28:08+09:00
ことりんと一緒 Springもね - 8. リポジトリ層 - 補足:H2 Database
概要 / 説明
ことりんと一緒 Springもね - 8. リポジトリ層 では、リポジトリ層を設けて、データの永続化処理を抽象化して外部の永続化領域への操作を透過的に実施できるようにしました。
今回のケースでは、外部の永続化領域はインメモリでアプリケーションに組み込みで動作する H2 Database Engine を利用していました。
この H2 Database Engine には、データベース情報を確認するためのコンソールアプリケーションが組み込まれています。そちらを利用してデータベースの状態確認を行ってみます。
前提 / 環境
ランタイムバージョン
- Kotlin : 1.3.21
- SpringBoot : 2.1.1.RELEASE
Spring Dependencies
- Web
- JDBC
- JPA
- H2
- Actuator
開発環境
- OS : Mac
- IDE : IntelliJ IDEA
- Build : Gradle
手順 / 解説
application.yml の設定
SpringBoot の application.yml に以下のようにH2データベースが使用できるように構成情報を定義します。
データソース
Springアプリケーションからデータベースに接続するためのデータソース定義を以下のように実施します。
spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:app;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE username: guest password: guest
構成要素 内容 driver-class-name H2DBのドライバーライブラリ org.h2.Driver
url データベース接続URL(※フォーマットについて後述) jdbc:h2:mem:app;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE
username データベース接続ユーザ名 password データベース接続パスワード H2データベース接続URLフォーマット
H2データベースは、インメモリで動作するアプリケーション組み込み型の使用や、リモートサーバとして稼働するサーバモード、またファイルシステムにデータを書き出す動作など様々な動作モードがとれます。それぞれの動作モードに合わせた接続URLフォーマットとなります。
動作モード URLフォーマット サンプル 組み込みモード:インメモリ(プライベート) jdbc:h2:mem:
jdbc:h2:mem: 組み込みモード:インメモリ(Named) jdbc:h2:mem:<DB名>
jdbc:h2:mem:app 組み込みモード:ローカルファイル jdbc:h2:[file:][<ファイルパス>]<DB名>
jdbc:h2:file:/data/sample インメモリDBのドロップ防止 DB_CLOSE_DELAY=-1 jdbc:h2:mem:app;DB_CLOSE_DELAY=-1 サーバモード:TCP接続 jdbc:h2:tcp://<サーバアドレス>[:<ポート>]/[<ファイルパス>] jdbc:h2:tcp://localhost:8084/data/sample サーバモード:TLS接続 jdbc:h2:ssl://<サーバアドレス>[:<ポート>]/[<ファイルパス>] jdbc:h2:ssl://localhost/mem:test VM終了時のDB接続切断 DB_CLOSE_ON_EXIT=TRUE jdbc:h2:mem:app;DB_CLOSE_ON_EXIT=TRUE H2データベース
H2 データベースのコンソール画面を表示させるには以下の設定を行います。
spring: h2: console: enabled: trueH2 データベースのコンソール画面の表示
アプリケーションを起動した後、H2 データベースコンソール画面にアクセスしてみます。
$ ./gradlew bootRun > Task :bootRun . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.1.RELEASE) : : : 2019-02-26 16:05:13.358 INFO 7073 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 15 endpoint(s) beneath base path '/admin' 2019-02-26 16:05:13.457 INFO 7073 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2019-02-26 16:05:13.460 INFO 7073 --- [ main] i.p.s.simple.SimpleApplicationKt : Started SimpleApplicationKt in 5.577 seconds (JVM running for 5.998) <==========---> 83% EXECUTING [2m 46s] > :bootRun
http://localhost:[設定したポート番号]/h2-console
へアクセスし、H2 データベースの接続ユーザ/パスワードを入力して接続を行います。まとめ / 振り返り
SpringBoot と組み合わせて手軽に利用できるH2データベースを操作してみました。
データベースの機能自体に依存しないアプリケーション開発の場面では、インストール作業が不要で、簡単に使えるH2データベースは利用できる場面がいろいろとあるのではないでしょうか。今回のソース