How to Test Server-side Kotlin #kotlinfest

How to Test Server-side Kotlin #kotlinfest

2018/8/25 Kotlin Fest 2018 での発表資料です

発表者2名が作ったツール:

- https://github.com/maeharin/factlin
- https://github.com/suusan2go/kotlin-fill-class

0ff63a1cf64b2501566f37581000bd9b?s=128

Hidenori Maehara

August 25, 2018
Tweet

Transcript

  1. How to Test Server-side Kotlin #kotlinfest エムスリー株式会社 前原 秀徳 @maeharin

    エムスリー株式会社 鈴木 健太 @suusan2go 2018/8/25 Kotlin Fest 2018 1
  2. 自己紹介: 前原 秀徳 • GitHub / Twitterは @maeharin • 2014年5月にエムスリーに入社

    • エンジニアチームリーダー、グループ会社の取締役 を歴任後、2017年5月にサーバーサイドKotlinを用 いたレガシーシステムのリニューアルプロジェクトを 立ち上げ • 自慢はブログ記事がはてなブックマーク1200を超え たこと 2
  3. 自己紹介: 鈴木 健太 • GitHub / Twitterは @suusan2go • 2017年11月に入社して、リニューアルプロジェクトに

    参戦 • 前職では主にRails触ってました • リニューアルのプロジェクトでは、KotlinでAPIサーバ 書きつつ、フロントエンドはNuxt.js使って実装みたい なことを担当 • 自慢は娘(2歳)が可愛いことです。 3
  4. エムスリーご紹介 ・医療に関するWebサービスを多数展開 ・全世界で約400万人の医師会員 ・日本で約25万人の医師会員 4

  5. Kotlinイン・アクションの翻訳陣が技術顧問・エンジニアフェロー エムスリー株式会社 技術顧問 藤原 聖 エムスリー株式会社 エンジニアフェロー 長澤 太郎 5

  6. お話する内容 • 10年前のレガシーシステムをサーバサイドKotlinでリプレイスしました。 その中で培ったKotlinのテストに関する話をします。 • リプレイスしたレガシーシステムは2つ ◦ 1. 医師のキャリア支援システム ◦

    2. 薬剤師のキャリア支援システム ◦ 事業規模感: 年間売上合わせて100億円超 ◦ システムの規模感: それぞれテーブル数150〜200程度 ◦ いずれのシステムも10年前の技術スタック(独自Java FW、ViewはXSLT) 6
  7. 当然ながら... 7 テストがない

  8. テストのあるシステムへ • APIサーバをKotlin + Spring Boot、フロントをRailsとVue.js(Nuxt.js)で リプレイス ◦ テストのあるシステムへ ◦

    2017年5月から開始。リリースはほぼ完了 ◦ 約1500ケースのテストをKotlinで書いてきた 8
  9. 医師キャリア支援システム リニューアル後 REST APIサーバー 9

  10. 薬剤師キャリア支援システム リニューアル後 REST APIサーバー 10

  11. お話する内容 • 1部: Server-Side Kotlin testing libraries(前原) ◦ サーバーサイドKotlin開発時に有用な主要テストライブラリを紹介 ◦

    Kotlinで使う際のTipsやハマりポイントも紹介 • 2部: How do we test Server-Side Kotlin(鈴木) ◦ 実際のプロジェクトでどのようにテストを書いていたかを紹介 • 3部: Our test tools for Kotlin(鈴木) ◦ テストのために発表者が作ったツールを紹介 11
  12. お話する内容 今回の発表の対象範囲 サーバーサイドKotlinの単体テス ト、DBに接続したインテグレーション テストについてお話します。 E2Eテストの話やAndroidに特化し たテストの話は行いません。 12 アジャイルテスト -高品質を追求するアジャイルチームにおけるテストの視点

    - 増田 聡 氏 P8より引用 https://www.slideshare.net/satoshimasuda/ss-3241717 アジャイルテストの4象限
  13. 1部 Server-Side Kotlin testing libraries • サーバーサイドKotlin開発時に有用な主要テストライブラリを紹介 • Kotlinで使う際のTipsやハマりポイントも紹介 •

    ライブラリ選定の際の参考になれば嬉しいです 13 ※ 各ライブラリの最新verは2018/8/24時点の情報
  14. 主要テスティングライブラリ をざっと分類 ※もちろんこれ以外にも 様々なライブラリが存在 赤色背景: 本資料で解説する分類 青字: 私達が使ったライブラリ 14

  15. テスティングフレームワーク 15

  16. JUnit4 • https://github.com/junit-team/junit4 • 最新ver: 4.12 • Javaのテストライブラリのデファクトスタンダード • アサーションライブラリであるhamcrestが組み込まれている

    • 実績が多く、各種WAFの殆どが対応している 16
  17. JUnit4 - Spring Bootのテスト用スターターキットもJUnit4 • spring-boot-starter-test(ver2.0.4.RELEASE) 17

  18. JUnit4 - Kotlinでも問題なく使用することができる • @Testなどのアノテーション含め、Kotlinでも問題なく使用可能 • 一部アノテーションはKotlinで使う時少しだけ留意点あり(後述) class SampleTest {

    @Test fun `1 + 2 は 3`() { val actual = sum(1, 2) assertEquals(3, actual) } 18
  19. JUnit4 - Kotlin tips: JUnit4の@RuleをKotlinのプロパティにつけるとエラーになる • Kotlinのvalからはprivateなフィールドとpublicなgetterが生成される • @Ruleはそのままだと、フィールドに付与される(*) ◦

    * @Ruleの@Targetにより導かれる。詳細はKotlin公式Doc参照 ◦ http://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets • @Ruleをつけるフィールドはpublicでなければいけないが、上記の通りフィール ドがprivateなので、must be publicというエラーになる ◦ https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html class SampleTest { @Rule val myRule = MyRule() // org.junit.internal.runners.rules.ValidationError: The @Rule 'myRule' must be public. 19
  20. JUnit4 - 参考: Intellij IDEAでShow Kotlin Bytecodeした結果の抜粋 // Show Kotlin

    Bytecodeの結果抜粋 public final class SampleTest { private final LMyRule; myRule @Lorg/junit/Rule;() public final getMyRule()LMyRule; 20 privateなフィールドと publicなgetterが生成されている @Ruleはprivateなフィールドの方 に付与されている
  21. JUnit4 - 対策1: @JvmFieldをつける • @JvmFieldをつけるとpublicなフィールドが生成されるようになるため、 正常に動作するようになる // Kotlin class

    SampleTest { @JvmField @Rule val myRule = MyRule() ↓ // Show Kotlin Bytecodeの結果抜粋 public final class SampleTest { public final LMyRule; myRule @Lorg/junit/Rule;() 21
  22. JUnit4 - 対策2: @get:Ruleとする • @get:Ruleとするとgetterへのアノテーションであることを明示できる。@Rule はpublicなgetterにつけても動作するため、これでもOK • https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets //

    Kotlin class SampleTest { @get:Rule val myRule = MyRule() ↓ // Show Kotlin Bytecodeの結果抜粋 public final class SampleTest { private final LMyRule; myRule public final getMyRule()LMyRule; @Lorg/junit/Rule;() 22
  23. JUnit4 - Kotlin tips:@BeforeClassなどJavaのstaticメソッドにつけるアノテーション • companion objectのメソッドに@BeforeClassなどのアノテーションをつ けるだけでは、何も実行されない • @BeforeClassはJavaのpublicなstaticメソッドにつけるアノテーション

    だが、上記ではpublicなstaticメソッドとしては認識されていないから (Companionという参照を介する必要がある) class SampleTest { companion object { @BeforeClass fun foo() { println("foo") } // 実行されない } @Test fun `test 1`() {} @Test fun `test 2`() {} 23
  24. JUnit4 - 対策: @JvmStaticをつける • @JvmStaticをつける。これによりJava側からstaticメソッドとして呼び出 せるようになる • 上記の例だとSampleTest.foo()として呼び出せるようになるので、JUnit が正常に動作する

    class SampleTest { companion object { @JvmStatic @BeforeClass fun foo() { println("foo") } } @Test fun `test 1`() {} @Test fun `test 2`() {} 24
  25. JUnit4 - Kotlin tips:@DataPointsなどJavaのstaticフィールドにつけるアノテーション • companion objectのプロパティにするだけではエラー • Java側からはprivateなstaticフィールドとして扱われるので、msut be

    publicというエラーになる @RunWith(Theories::class) class TheoriesTest { companion object { @DataPoints val values = listOf( Data(1, 2, 3), Data(1, -2, -1) ) // DataPoint field values must be public 25
  26. @RunWith(Theories::class) class TheoriesTest { companion object { @JvmField @DataPoints val

    values = listOf( Data(1, 2, 3), Data(1, -2, -1) ) } JUnit4 - 対策: @JvmFieldをつける • @JvmFieldをつける。するとJava側からはpublicなstaticフィールドとし て扱えるようになるので、JUnitが正常に動作する 26
  27. JUnit4 - KotlinでもJUnit4は問題なく使える • Javaとの相互運用でハマったらIntellij IDEAでShow Kotlin Bytecodeするとヒ ントがあるかも •

    Tools > Kotlin > Show Kotlin Bytecode 27
  28. JUnit5 • https://github.com/junit-team/junit5 • 最新ver: 5.2.0 • JUnit4の後継だが、JUnit4とは全く異なるフレームワーク ◦ Lambda式対応。JUnit4のRuleやテストランナーの廃止など

    ◦ JUnit Vintageを使えばJUnit4のテストケースの実行も可能 28
  29. // Springが提供するJUnit5用拡張 @ExtendWith(SpringExtension::class) @SpringBootTest class UserServiceTest { @Autowired lateinit var

    userService: UserService @Autowired lateinit var dataSource: DataSource JUnit5 - Spring5はJUnit5を公式サポート • https://docs.spring.io/spring/docs/current/spring-framework-refer ence/testing.html#integration-testing-annotations-junit-jupiter 29
  30. import org.junit.jupiter.api.assertAll class Junit5BuildinKotlinHelperTest { @Test fun `junit5のassertAllをKotlinで実行`() { assertAll("number",

    { assertEquals(1, 1) }, { assertEquals(2, 2) } ) JUnit5 - JUnit5はKotlin用のヘルパーを提供している • org.junit.jupiter.apiパッケージのトップレベル関数としてKotlinのヘル パーが定義されている ◦ https://junit.org/junit5/docs/current/user-guide/ 30
  31. Spek • https://github.com/spekframework/spek • 現在2.x準備中 ◦ 1系 最新ver: 1.2.0 ◦

    2系 最新ver: 2.0.0-alpha.1 • Kotlinで書かれたテスティングフレームワーク。BDDスタイル • JetBrainsの中の人が作っていた • アサーションは含まないので、好きなアサーションライブラリと組み合わ せて使う 31
  32. class SampleTest: Spek({ describe("userが0の時") { val repo = UserMemoryRepository() it("ユーザー数は0")

    { assertEquals(0, repo.count()) } it("存在しないuserを取得しようとするとnull") { assertNull(repo.findById(1)) } } 32 Spek - コード例
  33. Spek - Spek 2.xは... • KotlinConf 2017の発表(Testing Kotlin at Scale:

    Spek)では、「1.xは 使わず2.xを待った方がよく、2.xは2017年中に出そう」とのことであった が、まだ安定版はリリースされていない(2018/8/24の時点では、 v2.0.0-alpha.1) ◦ https://www.youtube.com/watch?v=R425cc6XrvA 33
  34. KotlinTest • https://github.com/kotlintest/kotlintest • 最新ver: 3.1.9 • Kotlinで書かれたBDDテスティングフレームワーク • 様々な書き方を選べる

    • アサーションも含んでいる。JUnitをテスティングフレームワークとして使 い、アサーションにKotlinTestを使うという手もある • Kotlin標準のkotlin.testシリーズとは別物 34
  35. class SampleTest: StringSpec() { init { "userが0の時" { val repo

    = UserMemoryRepository() "ユーザー数は0" should { repo.count() shouldBe 0 } "存在しないuserを取得しようとするとnull" should { repo.findById(1) shouldBe null } 35 KotlinTest - コード例
  36. KotlinTest - Spring用の拡張も用意されている • https://github.com/kotlintest/kotlintest/blob/master/doc/reference.md#spring 36

  37. 現在のIntellij IDEAでは クラスごとにテスト実行は できるが、テストケースごとには実 行できない模様 ※ 以下のverで確認 ・Intellij IDEA ULTIMATE

    2018.2 ・KotlinTest 3.1.9 37 KotlinTest - Intellij IDEAからのテスト実行
  38. その他のテスティングフレームワーク • TestNG ◦ https://github.com/cbeust/testng ◦ JUnit4と同じく歴史のあるテスティングフレームワーク • Spock ◦

    https://github.com/spockframework/spock ◦ groovy製 ◦ power-assertが一つの特徴 38
  39. 私達はJUnit4を利用 • spring-boot-starter-testのデフォルトに従った • 開発当初Spring Boot1.5系が最新だった(JUnit5未サポートだった) • 自分が使用するWebアプリケーションフレームワークとの相性、IDEとの相性を 調査してから導入することをオススメ 39

  40. アサーションライブラリ 40

  41. Hamcrest • 基本的なアサーションはJUnit4に組み込まれている • 応用的なアサーションはhamcrest-libraryを別途インストール • Kotlinで使う場合はisが予約語なのでエスケープする必要あり assertThat(a, `is`(b)) //

    もしくは import org.hamcrest.CoreMatchers.`is` as Is assertThat(a, Is(b)) 41
  42. AssertJ • fluent(流れるような)インターフェース • Kotlinで使うには特にハックが必要ない assertThat(a).isEqualTo(b) 42

  43. KotlinTestのアサーション • KotlinTestはテスティングフレームワークだが、アサーションも含んでい る • 3系からはモジュールが3つに別れており、アサーションである kotlintest-assertionsだけimportして使える ◦ https://github.com/kotlintest/kotlintest/blob/65aabf75d0ef0eac7d67fed1f11c 1932b41993ce/CHANGELOG.md#version-30x---march-29-2018

    • テスティングフレームワークとしてJUnit4を使い、アサーションに KotlinTestを使うということも可能 a shouldBe b 43
  44. Kotlin製のアサーションライブラリは沢山ある // kotlin.test assertEquals(actual = a, expected = b) //

    Kluent a shouldEqual b // HamKrest assert.that(a, equalTo(b)) // Expekt a.should.equal(b) // assertk assertThat(a).isEqualTo(b) 44
  45. (参考)私達はAssertJを使った • spring-boot-starter-testのデフォルトに従った • fluentインターフェースが好みだったため(主観) • どのライブラリを選ぶのかは好みが分かれる所だと思うが、一つのプロ ジェクト内で複数のアサーションライブラリが乱立しないようにはした方 がよいと思う 45

  46. モックライブラリ 46

  47. Mockito • https://github.com/mockito/mockito • 最新ver: 2.21.3 • Javaのモックライブラリのデファクトスタンダード • Kotlinで使う際のハマりポイント2つ

    ◦ 1,finalクラス/メソッドをモックできない問題 ◦ 2,Mockito.any()問題 47
  48. Mockito - 問題1. Mockitoはfinalクラス/メソッドをモックできない • Mockitoはfinalクラス/メソッドをモックできない • 一方、Kotlinのクラス/メソッドはデフォルトでfinal • 従って、Kotlinのクラス/メソッドをそのままmockしようとすると「final

    classのためモックできません」というエラーになる • 主な対処法4つ class ApiClient { fun foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) // Mockito cannot mock/spy because : - final class 48
  49. Mockito - 対処法1: 愚直にopen open class ApiClient { open fun

    foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) • モックする対象のクラス、メソッドを手動でopenする 49
  50. Mockito - 対処法2: インターフェースを使う interface ApiClient { fun foo(): String

    } class ApiClientImpl: ApiClient { override fun foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) • テスト対象クラスにインターフェースを実装させ、インターフェースをモッ クする 50
  51. Mockito - 対処法3: allopenプラグイン • Kotlinのallopenプラグインを使い、モック対象をopenする • Springの場合、kotlin-springプラグインを使ってSpringのBeanはopen しているケースがほとんどだと思うので、あまり意識することは無いかも しれない

    ◦ https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support 51
  52. // 独自アノテーションを作る package com.maeharin annotation class OpenForMock // モック対象のクラスに独自アノテーションを付与 @com.maeharin.OpenForMock

    class ApiClient { fun foo(config: ApiConfig): String { return "real" } } Mockito - 対処法3: allopenプラグイン 52
  53. Mockito - 対処法3: allopenプラグイン // build.gradle buildscript { dependencies {

    classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" } } apply plugin: 'kotlin-allopen' allOpen { // 独自アノテーションをallOpenの対象に指定 annotation('com.maeharin.OpenForMock') } 53
  54. Mockito - 対処法4: Mockitoのmock-maker-inlineを有効化 mock-maker-inline src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker に以下のテキストを書く • mockito2.1.0からexperimentalな機能として提供されている ◦

    https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2#mock- the-unmockable-opt-in-mocking-of-final-classesmethods • experimentalな機能ということは留意が必要 54
  55. Mockito - 問題2: NotNullな引数にMockito.any()を使うとランタイムでエラー • Mockito.any()の戻り値の型はプラットフォーム型のジェネリクス(T!)なのでコ ンパイルは通る • しかし、Mockito.any()はランタイムでnullを返す仕様。KotlinはNotNullな引 数に対してプラットフォーム型を渡すと、ランタイム時にメソッドをコールする

    前にnullチェックするため、エラーになる open class ApiClient { open fun foo(config: Config /** NotNullな引数 */){ … } } val mockClient = Mockito.mock(ApiClient::class.java) Mockito.`when`(mockClient.foo(Mockito.any())).thenReturn("dummy") // java.lang.IllegalStateException: Mockito.any() must not be null 55
  56. Mockito - 参考: Intellij IDEAでShow Kotlin Bytecodeした結果 mockClient.foo(Mockito.any()) ↓ INVOKESTATIC

    org/mockito/Mockito.any ()Ljava/lang/Object; DUP LDC "Mockito.any()" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V CHECKCAST ApiConfig INVOKEVIRTUAL ApiClient.foo (LApiConfig;)Ljava/lang/String; 56
  57. Mockito - 対処法1: 独自ヘルパーを作る private fun <T> myAny(): T {

    return Mockito.any() } Mockito.`when`(mock.foo(myAny())).thenReturn("dummy") • 戻り値の型がKotlinのジェネリクスとなるメソッドでMockito.any()をラップ。 すると、ランタイムのnullチェックがスキップされる ◦ https://stackoverflow.com/questions/30305217/is-it-possible-to-use-mockito-in-k otlin/30308199 57
  58. Mockito - 対処法2: Kotlin用ラッパーmockito-kotlinを使う import com.nhaarman.mockitokotlin2.* // mockito-kotlinのany() whenever(mock.foo(any())).thenReturn("dummy") •

    Mockito.any()ではなく、mockito-kotlinが提供するany()を使うと前述 のnullの問題は起きない 58
  59. Mockito-Kotlin • https://github.com/nhaarman/mockito-kotlin • MockitoのKotlin用ラッパー。あくまでもラッパーなので、基本機能は Mockitoを利用 • 1系と2系でパッケージ名異なる(現最新ver: 1.6.0 /

    2.0.0-RC1) • Kotlin用のDSLを提供(`when`と同義のwheneverなど) • mockito-kotlinのany()を使えば、前述のMockito.any()問題は起きない • ※前述のfinal問題を自動でカバーしてくれるわけではないので、そこは mockitoのmock-maker-inlinを有効化など前述の対策を講じる必要あ り 59
  60. import com.nhaarman.mockitokotlin2.* // モック作成 val mock: ApiClient = mock {

    on { foo(any()) } doReturn "dummy" } // モックを注入して実行 SampleService(mock).exec(ApiConfig()) // モックのメソッドが呼ばれたことを検証 verify(mock).foo(any()) Mockito-Kotlin - コード例 60
  61. MockK • https://github.com/mockk/mockk • Kotlin製のモックライブラリ • 最新ver: 1.8.6 • デフォルトでfinalクラス/メソッドをモックできる

    • mockkのany()を使えばMockito.any()問題は起きない 61
  62. import io.mockk.* // モック作成 val mock:ApiClient = mockk() every {

    mock.foo(any()) } returns "dummy" // モックを注入して実行 SampleService(mock).exec(ApiConfig()) // モックのメソッドが呼ばれたことを検証 verify(exactly = 1) { mock.foo(any()) } MockK - コード例 62
  63. 私達はMockito-Kotlinを利用 • spring-boot-starter-testのデフォルトはMockito • Mockito.any()問題の解消やKotlin用のDSLを提供してくれるのが便利 なので、MockitoのラッパーであるMockito-Kotlinを使うことにした 63

  64. DBセットアップライブラリ 64

  65. DBUnit • http://dbunit.sourceforge.net/ • 最新ver: 2.6.0 • XML、CSVなどからテストデータをセットアップできる • データベースの状態を検証することもできる

    • 2002年にver1.0がリリースされた歴史あるJavaライブラリ 65
  66. <!-- src/test/resources/dbunit-datas/users.xml --> <dataset> <users id="1" name="maeharin" job="ENGINEER" status="ACTIVE" age="30"

    score="0" is_admin="false" birth_day="1990-01-01" created_timestamp="2018-01-01 00:00:00" /> </dataset> 66 DBUnit - コード例
  67. @Before fun setup() { Class.forName(dbDriver) conn = DriverManager.getConnection(dbUrl, dbUser, dbPassword)

    val iDBConn = DatabaseConnection(conn) val iDataSet = FlatXmlDataSetBuilder().build(File("src/test/resources/dbunit-datas/users.xml")) DatabaseOperation.CLEAN_INSERT.execute(iDBConn, iDataSet) } @Test fun `test 1`() { val stmt = conn.createStatement() val rs = stmt.executeQuery("select * from users order by id asc") assertTrue(rs.next()) assertEquals("maeharin", rs.getString("name")) } 67 DBUnit - コード例
  68. DBUnit - 課題 • 重複記述が多くなる • 雛形を元に一部の値だけ変えたパターンをプログラマブルに作るといっ たことが行いづらい • カラムに変更があった時の変更箇所が多い

    68
  69. DbSetup • https://github.com/Ninja-Squad/DbSetup • 最新ver: 2.1.0 • xmlなどの外部ファイルではなく「コード」でテストデータを生成できる Javaライブラリ •

    Kotlin用のDSLも提供してくれている(DbSetup-kotlin) 69
  70. dbSetup(dest) { deleteAllFrom("users") insertInto("users") { mappedValues( "id" to 1, "name"

    to "maeharin", "job" to "ENGINEER", "status" to "ACTIVE", "age" to 30, "is_admin" to false, "created_timestamp" to LocalDateTime.now() ) } }.launch() 70 DbSetup - コード例
  71. 他の選択肢 • WAFの機能を利用する(例: Spring Testの@Sql) • O/Rマッパorデータアクセスライブラリのinsert系メソッドを利用する(例: MyBatis、Doma2、Exposed等) 71

  72. 私達はDbSetup-kotlinを少し工夫して利用 • (背景)私達はDBアクセスライブラリにはDoma2を使っている ◦ 私達はDomaのEntityはKotlinではなくJavaで定義した ◦ DomaのDaoを使ってテストデータをinsertするという手もあるが、Javaなのでフィクス チャのプロパティのIDE補完がされない • DbSetupと自作ツールであるFacltinを使うことで、Kotlinのdata

    classを 用いてプログラマブルにテストデータを生成する仕組みを作った ◦ 詳細は第3部で紹介 72
  73. 1部まとめ • テスティングライブラリは、WAFとIDEとの相性も加味して決めることをオ ススメ • アサーションはHamcrestやAsserJが鉄板だが、プロジェクトの中で乱 立しないようにだけ留意しつつ、好みのライブラリを使えばよいと思う • モックはKotlinまわりで少しハマりポイントがあるため、mockito-kotlinを 使うとよい(もしくはmockk)

    • DBテストセットアップはDbSetup+Factlin(後述)がオススメ 73
  74. 2部 How do we test Server-side Kotlin • 実際のプロジェクトでどのようにテストを書いていたか紹介します ◦

    テスト対象のアプリケーションについて ◦ テスト戦略について ◦ アプリケーションサービス、JSON APIのテストについてのTips 74
  75. テスト対象のアプリケーション構成についておさらい • SpringBoot + Kotlin で実装されたAPIサーバ • 10年弱運用されてきたアプリケーションのリニューアル!(Java独 自FW) •

    新アプリケーションはオニオンアーキテクチャをベースにした構成 ◦ プレゼンテーション層 ◦ アプリケーション層 ◦ ドメイン層 ◦ インフラストラクチャ層 75
  76. Kotlin + Spring Boot API サーバ REST APIでやりとり もしくは バックエンドのAPIとしてSpring

    Boot + Kotlin 76
  77. オニオンアーキテクチャ [DDD]ドメイン駆動 + オニオンアーキテクチャ概略 より引用 77

  78. パッケージ構成 78 • presentation ◦ WEBのエンドポイント(controllerとか) • domain ◦ ドメインエンティティ、バリューオブジェクト

    • application ◦ アプリケーションサービス(ユースケース) • infrastructure ◦ DBアクセス、他サービスとのREST API連携など
  79. プレゼンテーションの実装イメージ @RestController class TaskController(val taskService: TaskService) { @GetMapping("/tasks/{id}") fun findTask(@PathVariable("id")

    id: Int): TaskResponse { val task = taskService.findById(id) return TaskResponse(id = task.id, title = task.title) } } // Responseのクラスを必ず作成し、ドメイン層のクラスをそのまま返さない data class TaskResponse(val id: Int, val title: Title) 79
  80. アプリケーションサービスの実装イメージ @Service class TaskService( // infrastructure層に定義したクラスをコンストラクタインジェクションで DIする private val taskRepository:

    TaskRepository, private val userRepository: UserRepository ) { fun assignTask(taskId: TaskId, assigneeId: UserId): Task { val task = taskRepository.findById(taskId) ?: throw NotFoundExcpetiopn() val assignee = userRepository.findById(assigneeId) ?: throw NotFoundException() task.assignedTo(assignee)     return taskRepository.save(task) } } 80
  81. Repositoryの実装イメージ @Repository class DomaPrefectureRepository( private val prefectureEntityDao: PrefectureEntityDao ): PrefectureRepository

    { override fun findAll(): List<Prefecture> { val entities = prefectureEntityDao.selectAll() // Domaから取得 return entities.map { // Domaから取得したものを Domain層で定義したクラスにマッピングしなおす Prefecture( id = it.id, name = it.name, status = PrefectureStatus.valueOf(it.status), // レガシーなカラムをそのまま使わずにマッピング isPrimary = it.primaryFlag == 2 // レガシーなカラムをそのまま使わずにマッピング ) } } } 81
  82. このアーキテクチャのpros • 各レイヤの責務が明確になるので変更に強い ◦ ある箇所を変更したときの影響範囲が明確 • 技術的な関心事とビジネスロジックをある程度切り離して考えられる ◦ 特に歴史的な経緯を含むDBのスキーマとビジネスロジックを切り離 して考えられるようになった

    82
  83. このアーキテクチャのcons • 記述量は比較的に増える傾向にある ◦ 特に各レイヤー間でのマッピングは結構大変なので、開発当初は 結構辛い・・・ • 単純な処理しかしないAPIでは、単純にデータを各レイヤで受け渡すだ けになることがある 83

  84. pros/consを踏まえたテスト方針 • テスト対象は基本的にアプリケーションサービスより上の層 ◦ ※ドメイン層はロジックの複雑なものだけテスト • アプリケーションサービスに対するテスト ◦ アプリケーションとして要求される仕様を満たしているかをテスト ◦

    基本的にこの層を厚めに書くことで仕様の正しさを担保でき、ドメイ ン層のリファクタリングもしやすくする • JSON APIに対するテスト ◦ レスポンスの正しさ をテスト ◦ 意図したユーザーのみがアクセスできることをテスト 84
  85. Presentation Application Domain Infrastructure 各テストがカバーするレイヤーの範囲 JSON APIのテスト アプリケーションサービスのテスト PostgreSQL 外部API

    ElasticSearch 85
  86. アプリケーションサービスの テストにおけるTips 86

  87. Presentation Application Domain Infrastructure アプリケーションサービスのカバー範囲 モックしない PostgreSQL 外部API ElasticSearch 開発者毎に

    Docker モック アプリケーションサービスのテスト 87
  88. アプリケーションサービスのテストコード // アプリケーションサービスのテストコードサンプル class ConsultantServiceStopConsultantTest : ServiceTestBase() { // Testではコンストラクタインジェクションできないので

    lateinitでフィールドインジェクション @Autowired private lateinit var consultantService: ConsultantService @Autowired private lateinit var consultantRepository: ConsultantRepository @Autowired private lateinit var positionRepository: PositionRepository @Test fun コンサルタントアカウントを idで取得できること() { val consultantOutput = consultantService.showConsultantSimpleInfo(2) assertThat(consultantOutput.id).isEqualTo(2) } @Test fun コンサルタントアカウントを停止できること () { consultantService.stopConsultant(12) // テスト対象のサービス実行 // コンサルタントの求人が全て削除されていること val positions = positionRepository.findByConsultantId(12) assertThat(positions).isEmpty() } } 88
  89. アプリケーションサービスのテストコード @Test fun コンサルタントアカウントを idで取得できること() { val consultantOutput = consultantService.showConsultantSimpleInfo(2)

    assertThat(consultantOutput.id).isEqualTo(2) } @Test fun コンサルタントアカウントを停止できること () { consultantService.stopConsultant(12) // テスト対象のサービス実行 // コンサルタントの求人が全て削除されていること val positions = positionRepository.findByConsultantId(12) assertThat(positions).isEmpty() } 89
  90. アプリケーションサービスのテスト方針 • アプリケーションサービスの結合テスト ◦ DBアクセス => モックにしない • テストフレームワークはJUnit4。アサーションはAssertJ ◦

    当初はKotlinTestを使っていたが、Inttelijで実行したときに差 分をInttelijがいい感じにしてくれないので、AssertJに切り替 え • 戻り値をテストするのはもちろん、場合によってはDB変更もテスト する • テストでDBをどのように扱ったかをお話します。 90
  91. DBアクセスはモックにしなかった • アプリケーションの構造上、各Infrastructureレイヤーからドメインオブジェク トへのマッピングが必要になるので、少なくともその部分が例外にならない ことはテストしておきたい • 特にKotlinの場合 Null / Not

    Nullが厳密なので、DB上 Null を取りうるカラ ムの値を Not Null のプロパティにマッピングするようなコードがあると簡単 に実行時例外になってしまう • まだデータアクセスライブラリ(Doma2)によるSQL実行が最低限例外なく実 行できることも確認したい • マッピングのテストをかいた上でRepositoryをモックにする方向もあったが、 DbSetupを用いるとDB上のデータの生成が容易だったのでモックにしない 選択をした 91
  92. データのセットアップをどのようにおこなったか • DBのセットアップにはDbSetup-kotlinを使用 • DbSetup-Kotlinのコードは、Factlinというライブラリ(※後で紹介 します!)によりスキーマから自動生成 • Factlinにより、DBのデータセットアップもRailsのFactoryBotのよ うに平易に取り扱うことができるようになった 92

  93. // Factlinという独自ライブラリで作成した Fixtureクラスを活用して以下のようにセットアップできる @Before fun setUp() { dbSetup(DataSourceDestination(dataSource)) { init()

    // suusan2goユーザーを作成 insertUserFixture(UserFixture(name = "suusan2go", job = "engineer", age = 25)) // maeharinユーザーを作成 insertUserFixture(UserFixture(name = "maeharin", job = "engineer", age = 35)) }.launch() } Factlinを活用したDBデータのセットアップコード(※後ほど解説します) 93
  94. DBがどのように変更されたかをテストする • DBが意図した状態に変更されたことをチェックしたい ◦ 特にDBとドメインエンティティのマッピングが単純ではない場 合や、意図しないデータを更新していないか、意図した件数 更新しているかなどを厳密にチェックしたい場合 • AssertJ-DBを用いてDBの状態をテストした •

    AsserJ-DBはDBの変更をAssertJのようなシンタックスでテストで きるライブラリ 94
  95. @Test fun メッセージ送信上限をリセットできること() { // 特定テーブルの変更トラッキングを定義 val changes = Changes(Table(dataSource,

    "consultant")) // 変更のトラッキングを開始 changes.setStartPointNow() // テスト対象のサービスを実行 consultantService.resetConsultantRemainingMessageCount() // 変更のトラッキングを終了 changes.setEndPointNow() assertThat(changes).onTable("consultant") .hasNumberOfChanges(1) .change() .column("id").valueAtEndPoint().isEqualTo(someConsultant.id) .column("remaining_message_count").valueAtStartPoint().isEqualTo(0) .valueAtEndPoint().isEqualTo(4) } 95
  96. AssertJ-DBのcons • 細かなDBの変更を全てAssertJ-DBでテストするのは、シンタックス的に 少しつらい • 変更を検知するテーブルを絞っても、テストは通常よりも時間がかかる ようになる • 全てをAssertJ-DBでテストしているわけではなく、RepositoryでDBから 引き直してテストしているものが多いです

    96
  97. JSON APIのテストにおけるTips 97

  98. JSON APIのテストがカバーする範囲(おさらい) Presentation Application Domain Infrastructure モックしない PostgreSQL 外部API ElasticSearch

    開発者毎に Docker モック JSON APIのテスト 98
  99. JSON APIのテストのTIPS • Kotlinならではのテストリクエストの書き方 • JSON APIの効率的なテストの仕方 99

  100. JSON APIのテストをわかりやすく、宣言的に書く • 普通にSpringのMockMVCを使用してコントローラへの リクエストをテスト • Kotlinの拡張関数を活用すると認証が必要なAPIや、 どのようなリクエストなのかを宣言的に記述できます 100

  101. テストコード @Test fun コンサルタント一覧APIへのアクセステスト() { // 管理者はAPIにアクセスできる getはMockMvcRequestBuildersのStaticメソッド get("/admin/api/users").asUser(mockMvc, adminEmail,

    "password").also { request -> mockMvc.perform(request) .andExpect(status().isOk) // ステータスコードのチェック } // 普通のユーザーではアクセスできない get("/admin/api/users").asUser(mockMvc, email, "password").also { request -> mockMvc.perform(request) .andExpect(status().isForbidden) } // ログインしていないとアクセスできない get("/admin/api/users").also { request -> mockMvc.perform(request) .andExpect(status().isForbidden) } } 101
  102. 拡張関数の開設 // 事前にログイン用のAPIにアクセスして、ログイン状態のセッションを RequestBuilderに渡しておく fun MockHttpServletRequestBuilder.asUser( mockMvc: MockMvc, email: String,

    password: String ): MockHttpServletRequestBuilder { val loginRequest = LoginRequest(email, password) val session = MockHttpSession() val request = MockMvcRequestBuilders .post("/admin/api/login") .withJson(loginRequest) .accept(MediaType.APPLICATION_JSON_VALUE) .session(session) mockMvc.perform(request).andExpect(status().is2xxSuccessful) this.session(session) return this } 102
  103. // 渡されたオブジェクトを JSON化してリクエストボディに詰める fun MockHttpServletRequestBuilder.withJson( object: Object ): MockHttpServletRequestBuilder {

    this.contentType(APPLICATION_JSON_UTF_8) this.content(ObjectMapper().writeValueAsString(object) return this } 103
  104. 拡張関数の開設 val hogeRequestDto = HogeRequestDto(piyo = “hogehoge”, fuga = “hogehoge”)

    // Util的なものを使って書くとこんな感じになる val testRequest = post("/api/some_resources") RequestUtil.asUser(testRequest, email, password) RequestUtil.withJson(testRequest, hogeRequestDto) // 拡張関数を組み合わせて宣言的にリクエストを組み立てられる post("/api/some_resources") .asUser(mockMvc, email, password).withJson(hogeRequestDto) 104
  105. JSONのアサーション (JsonUnit) • spring-boot-starter-testではJSONassertが入ってくるが、JsonUnit の方が差分表示が見やすいため、JsonUnitを主に利用した ◦ https://github.com/skyscreamer/JSONassert ◦ https://github.com/lukas-krecan/JsonUnit 105

  106. // JsonUnit // どのキーの値がどう違うのかが明確 val a = "{\"id\":1,\"friends\":[1,2]}" val b

    = "{\"id\":1,\"friends\":[3,4]}" assertJsonEquals(a, b) // java.lang.AssertionError: JSON documents are different: //Different value found in node "friends[0]", expected: <1> but was: <3>. //Different value found in node "friends[1]", expected: <2> but was: <4>. 106
  107. • プロパティが数十個あってネストしているAPIの「期待値」を手で書いて いくのは骨が折れる • 最初のテスト実行時に正常なAPIレスポンスの期待値をファイルに保存 し、このファイルを元に期待するJSONフォーマットを記述した • このJSONファイルを期待値としてJsonUnitでアサートするようにする と、スピーディーにテストを書くことができた •

    また、これにより意図せずAPIの形が変わった場合にもテストで気がつ けるようになった 期待するJSONをどのように用意するか 107
  108. @Test fun `コンサルタント詳細情報を表示できること `() { var actual = mockMvc.perform(request) .andExpect(status().isOk)

    .andReturn().response.contentAsString assertEqualToJsonFileOrSave(actual, "path/to/expected/json") } // 独自ヘルパーメソッド // - jsonファイルがない状態で一度実行すると指定パス配下にファイルができる // - jsonファイルがない状態ではテスト自体は failする // - ファイルが有る場合は、JsonUnitで内容の一致をチェックする fun assertEqualToJsonFileOrSave(result: Any, path: String) 108
  109. おまけ:カバレッジ • カバレッジの取得にはJacocoを活用 • 少し前までData Classから自動生成されるコードについてのカバレッジ が0%になってしまうため、カバレッジがほぼ意味を成さない値になって いた • 0.8.2からはKotlinのテストでもカバレッジが正しく取得できる!

    109
  110. 例:こんなクラスのカバレッジを考える data class GeoCode( val latitude: Double, val longitude: Double

    ) { fun calculateDistances(other: GecCode): Long {} } @Test fun 正しく距離が計算されること(){ val geoCode = GeoCode(xxx, yyy) val otherGeoCode = GeoCode(zzzz, aaaa) assertEquals(geoCode.calculateDistance(otherGeoCode), xxxx) } 110
  111. 0.8.1以前 111

  112. 0.8.2以降 112

  113. 0.8.2はなんと今週リリース 113

  114. How do we test Servier-Side Kotlinのまとめ • アプリケーションサービスより上の層を中心にテストしてます • アプリケーションサービスの結合テストのTips

    ◦ DBSetupでのデータセットアップ ◦ AssertJ-DBを使ったデータベース状態のテスト • JSON APIでのテストのTips ◦ 拡張関数でテスト用のリクエストを宣言的に記述できる ◦ JSONUnit & JSONファイル生成でJSONのテストを容易に • テストのカバレッジはjacocoで。 0.8.2からはData Classもカバレッジが正しく取得できるように! 114
  115. 3部 Our test tools for Kotlin • テストのために発表者が作ったツールたちの紹介 ◦ Factlin

    ◦ kotlin-fill-class 115
  116. Factlin (by @maeharin) • DbSetupのシンタックスは平易とはいえ、カラムの多いテーブルに対 するセットアップコードを一から書くのは大変 • Factlinを使うことで、既存のデータベーススキーマから、DbSetup用 のコードを生成できる •

    テストフィクスチャ用のクラスでNull / Not Nullを区別できる • テストフィクスチャ用のクラスはKotlinのdata classになっているた め、copyメソッドを用いて「少しだけパラメータが異なる似たようなフィ クスチャ」を簡単に生成できる 116
  117. Factlinの生成コード例 CREATE TABLE users ( id SERIAL PRIMARY KEY, name

    VARCHAR(256) NOT NULL, job VARCHAR(256) NOT NULL DEFAULT 'engineer', status VARCHAR(256) NOT NULL DEFAULT 'ACTIVE', age INTEGER NOT NULL, score NUMERIC NOT NULL, is_admin BOOLEAN NOT NULL, birth_day DATE NOT NULL, nick_name VARCHAR(256), created_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, updated_timestamp TIMESTAMP WITHOUT TIME ZONE ); • 左記のようなスキーマに対して、 factlinのgradle タスクを実行する   ./gradlew factliin 117
  118. // DBのカラムへのコメントも自動で反映されます! // Fixture Class data class UsersFixture ( val

    id: Int = 0, // primary key val name: String = "", // user name val job: String = "", // job name val status: String = "", // activate status val age: Int = 0, // user age val score: BigDecimal = 0.toBigDecimal(), // game score val is_admin: Boolean = false, // user is admin user or not val birth_day: LocalDate = LocalDate.now(), // user birth day val nick_name: String? = null, // nick name val created_timestamp: LocalDateTime = LocalDateTime.now(), val updated_timestamp: LocalDateTime? = null ) 118
  119. // DbSetupの拡張関数を自動生成 fun DbSetupBuilder.insertUsersFixture(f: UsersFixture) { insertInto("users") { mappedValues( "id"

    to f.id, "name" to f.name, "job" to f.job, "status" to f.status, "age" to f.age, "score" to f.score, "is_admin" to f.is_admin, "birth_day" to f.birth_day, "nick_name" to f.nick_name, "created_timestamp" to f.created_timestamp, "updated_timestamp" to f.updated_timestamp ) } } 119
  120. // Factlinという独自ライブラリで作成した Fixtureクラスを活用して以下のようにセットアップできる @Before fun setUp() { dbSetup(DataSourceDestination(dataSource)) { init()

    // suusan2goユーザーを作成 insertUserFixture(UserFixture(name = "suusan2go", job = "engineer", age = 25)) // maeharinユーザーを作成 insertUserFixture(UserFixture(name = "maeharin", job = "engineer", age = 35)) }.launch() } Factlinを活用したDBデータのセットアップコード 120
  121. 日本語の解説記事あります • 以下をご参照ください!!!! • DBスキーマからKotlinのテストフィクスチャを自動生成するgradleプラグ インを作った 121

  122. Kotlin Fill Class (by @suusan2go) • コンストラクタを書くのを少しだけ楽にしてくれるIntellij Plugin • Named

    argumentsの形式でコンストラクタの呼び出しを補完してくれる • Kotlin標準のクラスならデフォルト値もついでに与えてくれる 122
  123. モチベーション • 小さなクラスならよいが、歴史的な経緯により大量のプロパティを持 たざるえないクラスはテストのためにコンストラクタを呼び出して初期 化するのが大変しんどい • テストのスコープからは少し外れるが、我々のプロジェクトでは各レ イヤ間でクラス間で値をマッピングするコードがとても多かった。 Intellij IDEAでは補完、足りないプロパティの警告はしてくれるもの

    の、一つずつnamed arguments形式で書くのがだるい 123
  124. たとえばこんなクラスをインスタンス化するコード書くの辛くないですか data class User( val id: Int, val name: String,

    val age: Int, val address: String, val firstName: String, val lastName: String, val firstNameKana: String, val lastNameKana: String, val favoriteSongName: String, val favoriteFruitsName: String, val favoriteSport: String, val favoriteProgramingLanguage: String, val favoriteBook: String, val isMarried: Boolean, val hasDependent: Boolean, val CommutingTime: Int, val nearestStationName: String, val isAdmin: Boolean, val educationBackground: String, val githubAccount: String, val twitterAccount: String, val facebookAccount: String, val linkedinAccount: String, val lineAccount: String, val m3Account: String, val birthday: LocalDate, val motherTongue: String, val occupation: String, val bestSubject: String, val deathblow: String, val youtubeAccount: String, val lovesKotlin: Boolean ) 124
  125. Kotlin Fill Classを使わない場合 125

  126. Kotlin Fill Classのデモ 126

  127. Kotlin Fill Classの実装について • Inttelijプラグインの作り方自体は今回の発表のスコープから外れるので以下を ご参照ください!!! • そしてダウンロードしてください!!! • まだ機能は荒削りなのでpulll-requestお待ちしてます!

    • Kotlinでコンストラクタをシュッと書くためにIntellijのプラグインを作った 127
  128. 3部 Our test tools for kotlinのまとめ • 以下のようなツールで開発の効率化を行いました ◦ Factlin

    DBのスキーマからDBSetupのコードを自動生成 ◦ 大量のプロパティを持つクラスの生成にkotlin-fill-class • KotlinはJavaの資産が使えるとはいえ、まだまだこういったKotlin用のツー ルが少ない印象なので今がチャンス!かもしれません。 128
  129. 最後に • サーバーサイドKotlinでもテストはバッチリ書けます! • エムスリーではサーバーサイドKotlinを用いたプロジェクトが複数立ち上 がっています。We’re Hiring!!! 129