Slide 1

Slide 1 text

How to Test Server-side Kotlin #kotlinfest エムスリー株式会社 前原 秀徳 @maeharin エムスリー株式会社 鈴木 健太 @suusan2go 2018/8/25 Kotlin Fest 2018 1

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

自己紹介: 鈴木 健太 ● GitHub / Twitterは @suusan2go ● 2017年11月に入社して、リニューアルプロジェクトに 参戦 ● 前職では主にRails触ってました ● リニューアルのプロジェクトでは、KotlinでAPIサーバ 書きつつ、フロントエンドはNuxt.js使って実装みたい なことを担当 ● 自慢は娘(2歳)が可愛いことです。 3

Slide 4

Slide 4 text

エムスリーご紹介 ・医療に関するWebサービスを多数展開 ・全世界で約400万人の医師会員 ・日本で約25万人の医師会員 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

お話する内容 ● 10年前のレガシーシステムをサーバサイドKotlinでリプレイスしました。 その中で培ったKotlinのテストに関する話をします。 ● リプレイスしたレガシーシステムは2つ ○ 1. 医師のキャリア支援システム ○ 2. 薬剤師のキャリア支援システム ○ 事業規模感: 年間売上合わせて100億円超 ○ システムの規模感: それぞれテーブル数150〜200程度 ○ いずれのシステムも10年前の技術スタック(独自Java FW、ViewはXSLT) 6

Slide 7

Slide 7 text

当然ながら... 7 テストがない

Slide 8

Slide 8 text

テストのあるシステムへ ● APIサーバをKotlin + Spring Boot、フロントをRailsとVue.js(Nuxt.js)で リプレイス ○ テストのあるシステムへ ○ 2017年5月から開始。リリースはほぼ完了 ○ 約1500ケースのテストをKotlinで書いてきた 8

Slide 9

Slide 9 text

医師キャリア支援システム リニューアル後 REST APIサーバー 9

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

お話する内容 ● 1部: Server-Side Kotlin testing libraries(前原) ○ サーバーサイドKotlin開発時に有用な主要テストライブラリを紹介 ○ Kotlinで使う際のTipsやハマりポイントも紹介 ● 2部: How do we test Server-Side Kotlin(鈴木) ○ 実際のプロジェクトでどのようにテストを書いていたかを紹介 ● 3部: Our test tools for Kotlin(鈴木) ○ テストのために発表者が作ったツールを紹介 11

Slide 12

Slide 12 text

お話する内容 今回の発表の対象範囲 サーバーサイドKotlinの単体テス ト、DBに接続したインテグレーション テストについてお話します。 E2Eテストの話やAndroidに特化し たテストの話は行いません。 12 アジャイルテスト -高品質を追求するアジャイルチームにおけるテストの視点 - 増田 聡 氏 P8より引用 https://www.slideshare.net/satoshimasuda/ss-3241717 アジャイルテストの4象限

Slide 13

Slide 13 text

1部 Server-Side Kotlin testing libraries ● サーバーサイドKotlin開発時に有用な主要テストライブラリを紹介 ● Kotlinで使う際のTipsやハマりポイントも紹介 ● ライブラリ選定の際の参考になれば嬉しいです 13 ※ 各ライブラリの最新verは2018/8/24時点の情報

Slide 14

Slide 14 text

主要テスティングライブラリ をざっと分類 ※もちろんこれ以外にも 様々なライブラリが存在 赤色背景: 本資料で解説する分類 青字: 私達が使ったライブラリ 14

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

JUnit4 ● https://github.com/junit-team/junit4 ● 最新ver: 4.12 ● Javaのテストライブラリのデファクトスタンダード ● アサーションライブラリであるhamcrestが組み込まれている ● 実績が多く、各種WAFの殆どが対応している 16

Slide 17

Slide 17 text

JUnit4 - Spring Bootのテスト用スターターキットもJUnit4 ● spring-boot-starter-test(ver2.0.4.RELEASE) 17

Slide 18

Slide 18 text

JUnit4 - Kotlinでも問題なく使用することができる ● @Testなどのアノテーション含め、Kotlinでも問題なく使用可能 ● 一部アノテーションはKotlinで使う時少しだけ留意点あり(後述) class SampleTest { @Test fun `1 + 2 は 3`() { val actual = sum(1, 2) assertEquals(3, actual) } 18

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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なフィールドの方 に付与されている

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

@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

Slide 27

Slide 27 text

JUnit4 - KotlinでもJUnit4は問題なく使える ● Javaとの相互運用でハマったらIntellij IDEAでShow Kotlin Bytecodeするとヒ ントがあるかも ● Tools > Kotlin > Show Kotlin Bytecode 27

Slide 28

Slide 28 text

JUnit5 ● https://github.com/junit-team/junit5 ● 最新ver: 5.2.0 ● JUnit4の後継だが、JUnit4とは全く異なるフレームワーク ○ Lambda式対応。JUnit4のRuleやテストランナーの廃止など ○ JUnit Vintageを使えばJUnit4のテストケースの実行も可能 28

Slide 29

Slide 29 text

// 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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Spek ● https://github.com/spekframework/spek ● 現在2.x準備中 ○ 1系 最新ver: 1.2.0 ○ 2系 最新ver: 2.0.0-alpha.1 ● Kotlinで書かれたテスティングフレームワーク。BDDスタイル ● JetBrainsの中の人が作っていた ● アサーションは含まないので、好きなアサーションライブラリと組み合わ せて使う 31

Slide 32

Slide 32 text

class SampleTest: Spek({ describe("userが0の時") { val repo = UserMemoryRepository() it("ユーザー数は0") { assertEquals(0, repo.count()) } it("存在しないuserを取得しようとするとnull") { assertNull(repo.findById(1)) } } 32 Spek - コード例

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

KotlinTest ● https://github.com/kotlintest/kotlintest ● 最新ver: 3.1.9 ● Kotlinで書かれたBDDテスティングフレームワーク ● 様々な書き方を選べる ● アサーションも含んでいる。JUnitをテスティングフレームワークとして使 い、アサーションにKotlinTestを使うという手もある ● Kotlin標準のkotlin.testシリーズとは別物 34

Slide 35

Slide 35 text

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 - コード例

Slide 36

Slide 36 text

KotlinTest - Spring用の拡張も用意されている ● https://github.com/kotlintest/kotlintest/blob/master/doc/reference.md#spring 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

その他のテスティングフレームワーク ● TestNG ○ https://github.com/cbeust/testng ○ JUnit4と同じく歴史のあるテスティングフレームワーク ● Spock ○ https://github.com/spockframework/spock ○ groovy製 ○ power-assertが一つの特徴 38

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

AssertJ ● fluent(流れるような)インターフェース ● Kotlinで使うには特にハックが必要ない assertThat(a).isEqualTo(b) 42

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

(参考)私達はAssertJを使った ● spring-boot-starter-testのデフォルトに従った ● fluentインターフェースが好みだったため(主観) ● どのライブラリを選ぶのかは好みが分かれる所だと思うが、一つのプロ ジェクト内で複数のアサーションライブラリが乱立しないようにはした方 がよいと思う 45

Slide 46

Slide 46 text

モックライブラリ 46

Slide 47

Slide 47 text

Mockito ● https://github.com/mockito/mockito ● 最新ver: 2.21.3 ● Javaのモックライブラリのデファクトスタンダード ● Kotlinで使う際のハマりポイント2つ ○ 1,finalクラス/メソッドをモックできない問題 ○ 2,Mockito.any()問題 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Mockito - 対処法1: 愚直にopen open class ApiClient { open fun foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) ● モックする対象のクラス、メソッドを手動でopenする 49

Slide 50

Slide 50 text

Mockito - 対処法2: インターフェースを使う interface ApiClient { fun foo(): String } class ApiClientImpl: ApiClient { override fun foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) ● テスト対象クラスにインターフェースを実装させ、インターフェースをモッ クする 50

Slide 51

Slide 51 text

Mockito - 対処法3: allopenプラグイン ● Kotlinのallopenプラグインを使い、モック対象をopenする ● Springの場合、kotlin-springプラグインを使ってSpringのBeanはopen しているケースがほとんどだと思うので、あまり意識することは無いかも しれない ○ https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support 51

Slide 52

Slide 52 text

// 独自アノテーションを作る package com.maeharin annotation class OpenForMock // モック対象のクラスに独自アノテーションを付与 @com.maeharin.OpenForMock class ApiClient { fun foo(config: ApiConfig): String { return "real" } } Mockito - 対処法3: allopenプラグイン 52

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Mockito - 対処法1: 独自ヘルパーを作る private fun 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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

import com.nhaarman.mockitokotlin2.* // モック作成 val mock: ApiClient = mock { on { foo(any()) } doReturn "dummy" } // モックを注入して実行 SampleService(mock).exec(ApiConfig()) // モックのメソッドが呼ばれたことを検証 verify(mock).foo(any()) Mockito-Kotlin - コード例 60

Slide 61

Slide 61 text

MockK ● https://github.com/mockk/mockk ● Kotlin製のモックライブラリ ● 最新ver: 1.8.6 ● デフォルトでfinalクラス/メソッドをモックできる ● mockkのany()を使えばMockito.any()問題は起きない 61

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

私達はMockito-Kotlinを利用 ● spring-boot-starter-testのデフォルトはMockito ● Mockito.any()問題の解消やKotlin用のDSLを提供してくれるのが便利 なので、MockitoのラッパーであるMockito-Kotlinを使うことにした 63

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

DBUnit ● http://dbunit.sourceforge.net/ ● 最新ver: 2.6.0 ● XML、CSVなどからテストデータをセットアップできる ● データベースの状態を検証することもできる ● 2002年にver1.0がリリースされた歴史あるJavaライブラリ 65

Slide 66

Slide 66 text

66 DBUnit - コード例

Slide 67

Slide 67 text

@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 - コード例

Slide 68

Slide 68 text

DBUnit - 課題 ● 重複記述が多くなる ● 雛形を元に一部の値だけ変えたパターンをプログラマブルに作るといっ たことが行いづらい ● カラムに変更があった時の変更箇所が多い 68

Slide 69

Slide 69 text

DbSetup ● https://github.com/Ninja-Squad/DbSetup ● 最新ver: 2.1.0 ● xmlなどの外部ファイルではなく「コード」でテストデータを生成できる Javaライブラリ ● Kotlin用のDSLも提供してくれている(DbSetup-kotlin) 69

Slide 70

Slide 70 text

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 - コード例

Slide 71

Slide 71 text

他の選択肢 ● WAFの機能を利用する(例: Spring Testの@Sql) ● O/Rマッパorデータアクセスライブラリのinsert系メソッドを利用する(例: MyBatis、Doma2、Exposed等) 71

Slide 72

Slide 72 text

私達はDbSetup-kotlinを少し工夫して利用 ● (背景)私達はDBアクセスライブラリにはDoma2を使っている ○ 私達はDomaのEntityはKotlinではなくJavaで定義した ○ DomaのDaoを使ってテストデータをinsertするという手もあるが、Javaなのでフィクス チャのプロパティのIDE補完がされない ● DbSetupと自作ツールであるFacltinを使うことで、Kotlinのdata classを 用いてプログラマブルにテストデータを生成する仕組みを作った ○ 詳細は第3部で紹介 72

Slide 73

Slide 73 text

1部まとめ ● テスティングライブラリは、WAFとIDEとの相性も加味して決めることをオ ススメ ● アサーションはHamcrestやAsserJが鉄板だが、プロジェクトの中で乱 立しないようにだけ留意しつつ、好みのライブラリを使えばよいと思う ● モックはKotlinまわりで少しハマりポイントがあるため、mockito-kotlinを 使うとよい(もしくはmockk) ● DBテストセットアップはDbSetup+Factlin(後述)がオススメ 73

Slide 74

Slide 74 text

2部 How do we test Server-side Kotlin ● 実際のプロジェクトでどのようにテストを書いていたか紹介します ○ テスト対象のアプリケーションについて ○ テスト戦略について ○ アプリケーションサービス、JSON APIのテストについてのTips 74

Slide 75

Slide 75 text

テスト対象のアプリケーション構成についておさらい ● SpringBoot + Kotlin で実装されたAPIサーバ ● 10年弱運用されてきたアプリケーションのリニューアル!(Java独 自FW) ● 新アプリケーションはオニオンアーキテクチャをベースにした構成 ○ プレゼンテーション層 ○ アプリケーション層 ○ ドメイン層 ○ インフラストラクチャ層 75

Slide 76

Slide 76 text

Kotlin + Spring Boot API サーバ REST APIでやりとり もしくは バックエンドのAPIとしてSpring Boot + Kotlin 76

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

パッケージ構成 78 ● presentation ○ WEBのエンドポイント(controllerとか) ● domain ○ ドメインエンティティ、バリューオブジェクト ● application ○ アプリケーションサービス(ユースケース) ● infrastructure ○ DBアクセス、他サービスとのREST API連携など

Slide 79

Slide 79 text

プレゼンテーションの実装イメージ @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

Slide 80

Slide 80 text

アプリケーションサービスの実装イメージ @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

Slide 81

Slide 81 text

Repositoryの実装イメージ @Repository class DomaPrefectureRepository( private val prefectureEntityDao: PrefectureEntityDao ): PrefectureRepository { override fun findAll(): List { 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

Slide 82

Slide 82 text

このアーキテクチャのpros ● 各レイヤの責務が明確になるので変更に強い ○ ある箇所を変更したときの影響範囲が明確 ● 技術的な関心事とビジネスロジックをある程度切り離して考えられる ○ 特に歴史的な経緯を含むDBのスキーマとビジネスロジックを切り離 して考えられるようになった 82

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

pros/consを踏まえたテスト方針 ● テスト対象は基本的にアプリケーションサービスより上の層 ○ ※ドメイン層はロジックの複雑なものだけテスト ● アプリケーションサービスに対するテスト ○ アプリケーションとして要求される仕様を満たしているかをテスト ○ 基本的にこの層を厚めに書くことで仕様の正しさを担保でき、ドメイ ン層のリファクタリングもしやすくする ● JSON APIに対するテスト ○ レスポンスの正しさ をテスト ○ 意図したユーザーのみがアクセスできることをテスト 84

Slide 85

Slide 85 text

Presentation Application Domain Infrastructure 各テストがカバーするレイヤーの範囲 JSON APIのテスト アプリケーションサービスのテスト PostgreSQL 外部API ElasticSearch 85

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

アプリケーションサービスのテストコード // アプリケーションサービスのテストコードサンプル 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

Slide 89

Slide 89 text

アプリケーションサービスのテストコード @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

Slide 90

Slide 90 text

アプリケーションサービスのテスト方針 ● アプリケーションサービスの結合テスト ○ DBアクセス => モックにしない ● テストフレームワークはJUnit4。アサーションはAssertJ ○ 当初はKotlinTestを使っていたが、Inttelijで実行したときに差 分をInttelijがいい感じにしてくれないので、AssertJに切り替 え ● 戻り値をテストするのはもちろん、場合によってはDB変更もテスト する ● テストでDBをどのように扱ったかをお話します。 90

Slide 91

Slide 91 text

DBアクセスはモックにしなかった ● アプリケーションの構造上、各Infrastructureレイヤーからドメインオブジェク トへのマッピングが必要になるので、少なくともその部分が例外にならない ことはテストしておきたい ● 特にKotlinの場合 Null / Not Nullが厳密なので、DB上 Null を取りうるカラ ムの値を Not Null のプロパティにマッピングするようなコードがあると簡単 に実行時例外になってしまう ● まだデータアクセスライブラリ(Doma2)によるSQL実行が最低限例外なく実 行できることも確認したい ● マッピングのテストをかいた上でRepositoryをモックにする方向もあったが、 DbSetupを用いるとDB上のデータの生成が容易だったのでモックにしない 選択をした 91

Slide 92

Slide 92 text

データのセットアップをどのようにおこなったか ● DBのセットアップにはDbSetup-kotlinを使用 ● DbSetup-Kotlinのコードは、Factlinというライブラリ(※後で紹介 します!)によりスキーマから自動生成 ● Factlinにより、DBのデータセットアップもRailsのFactoryBotのよ うに平易に取り扱うことができるようになった 92

Slide 93

Slide 93 text

// 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

Slide 94

Slide 94 text

DBがどのように変更されたかをテストする ● DBが意図した状態に変更されたことをチェックしたい ○ 特にDBとドメインエンティティのマッピングが単純ではない場 合や、意図しないデータを更新していないか、意図した件数 更新しているかなどを厳密にチェックしたい場合 ● AssertJ-DBを用いてDBの状態をテストした ● AsserJ-DBはDBの変更をAssertJのようなシンタックスでテストで きるライブラリ 94

Slide 95

Slide 95 text

@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

Slide 96

Slide 96 text

AssertJ-DBのcons ● 細かなDBの変更を全てAssertJ-DBでテストするのは、シンタックス的に 少しつらい ● 変更を検知するテーブルを絞っても、テストは通常よりも時間がかかる ようになる ● 全てをAssertJ-DBでテストしているわけではなく、RepositoryでDBから 引き直してテストしているものが多いです 96

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

JSON APIのテストのTIPS ● Kotlinならではのテストリクエストの書き方 ● JSON APIの効率的なテストの仕方 99

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

テストコード @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

Slide 102

Slide 102 text

拡張関数の開設 // 事前にログイン用の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

Slide 103

Slide 103 text

// 渡されたオブジェクトを JSON化してリクエストボディに詰める fun MockHttpServletRequestBuilder.withJson( object: Object ): MockHttpServletRequestBuilder { this.contentType(APPLICATION_JSON_UTF_8) this.content(ObjectMapper().writeValueAsString(object) return this } 103

Slide 104

Slide 104 text

拡張関数の開設 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

Slide 105

Slide 105 text

JSONのアサーション (JsonUnit) ● spring-boot-starter-testではJSONassertが入ってくるが、JsonUnit の方が差分表示が見やすいため、JsonUnitを主に利用した ○ https://github.com/skyscreamer/JSONassert ○ https://github.com/lukas-krecan/JsonUnit 105

Slide 106

Slide 106 text

// 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

Slide 107

Slide 107 text

● プロパティが数十個あってネストしているAPIの「期待値」を手で書いて いくのは骨が折れる ● 最初のテスト実行時に正常なAPIレスポンスの期待値をファイルに保存 し、このファイルを元に期待するJSONフォーマットを記述した ● このJSONファイルを期待値としてJsonUnitでアサートするようにする と、スピーディーにテストを書くことができた ● また、これにより意図せずAPIの形が変わった場合にもテストで気がつ けるようになった 期待するJSONをどのように用意するか 107

Slide 108

Slide 108 text

@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

Slide 109

Slide 109 text

おまけ:カバレッジ ● カバレッジの取得にはJacocoを活用 ● 少し前までData Classから自動生成されるコードについてのカバレッジ が0%になってしまうため、カバレッジがほぼ意味を成さない値になって いた ● 0.8.2からはKotlinのテストでもカバレッジが正しく取得できる! 109

Slide 110

Slide 110 text

例:こんなクラスのカバレッジを考える 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

Slide 111

Slide 111 text

0.8.1以前 111

Slide 112

Slide 112 text

0.8.2以降 112

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

How do we test Servier-Side Kotlinのまとめ ● アプリケーションサービスより上の層を中心にテストしてます ● アプリケーションサービスの結合テストのTips ○ DBSetupでのデータセットアップ ○ AssertJ-DBを使ったデータベース状態のテスト ● JSON APIでのテストのTips ○ 拡張関数でテスト用のリクエストを宣言的に記述できる ○ JSONUnit & JSONファイル生成でJSONのテストを容易に ● テストのカバレッジはjacocoで。 0.8.2からはData Classもカバレッジが正しく取得できるように! 114

Slide 115

Slide 115 text

3部 Our test tools for Kotlin ● テストのために発表者が作ったツールたちの紹介 ○ Factlin ○ kotlin-fill-class 115

Slide 116

Slide 116 text

Factlin (by @maeharin) ● DbSetupのシンタックスは平易とはいえ、カラムの多いテーブルに対 するセットアップコードを一から書くのは大変 ● Factlinを使うことで、既存のデータベーススキーマから、DbSetup用 のコードを生成できる ● テストフィクスチャ用のクラスでNull / Not Nullを区別できる ● テストフィクスチャ用のクラスはKotlinのdata classになっているた め、copyメソッドを用いて「少しだけパラメータが異なる似たようなフィ クスチャ」を簡単に生成できる 116

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

// 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

Slide 119

Slide 119 text

// 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

Slide 120

Slide 120 text

// 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

Slide 121

Slide 121 text

日本語の解説記事あります ● 以下をご参照ください!!!! ● DBスキーマからKotlinのテストフィクスチャを自動生成するgradleプラグ インを作った 121

Slide 122

Slide 122 text

Kotlin Fill Class (by @suusan2go) ● コンストラクタを書くのを少しだけ楽にしてくれるIntellij Plugin ● Named argumentsの形式でコンストラクタの呼び出しを補完してくれる ● Kotlin標準のクラスならデフォルト値もついでに与えてくれる 122

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

たとえばこんなクラスをインスタンス化するコード書くの辛くないですか 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

Slide 125

Slide 125 text

Kotlin Fill Classを使わない場合 125

Slide 126

Slide 126 text

Kotlin Fill Classのデモ 126

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

3部 Our test tools for kotlinのまとめ ● 以下のようなツールで開発の効率化を行いました ○ Factlin DBのスキーマからDBSetupのコードを自動生成 ○ 大量のプロパティを持つクラスの生成にkotlin-fill-class ● KotlinはJavaの資産が使えるとはいえ、まだまだこういったKotlin用のツー ルが少ない印象なので今がチャンス!かもしれません。 128

Slide 129

Slide 129 text

最後に ● サーバーサイドKotlinでもテストはバッチリ書けます! ● エムスリーではサーバーサイドKotlinを用いたプロジェクトが複数立ち上 がっています。We’re Hiring!!! 129