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. 1.

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

    エムスリー株式会社 鈴木 健太 @suusan2go 2018/8/25 Kotlin Fest 2018 1
  2. 2.

    自己紹介: 前原 秀徳 • GitHub / Twitterは @maeharin • 2014年5月にエムスリーに入社

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

    自己紹介: 鈴木 健太 • GitHub / Twitterは @suusan2go • 2017年11月に入社して、リニューアルプロジェクトに

    参戦 • 前職では主にRails触ってました • リニューアルのプロジェクトでは、KotlinでAPIサーバ 書きつつ、フロントエンドはNuxt.js使って実装みたい なことを担当 • 自慢は娘(2歳)が可愛いことです。 3
  4. 6.

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

    2. 薬剤師のキャリア支援システム ◦ 事業規模感: 年間売上合わせて100億円超 ◦ システムの規模感: それぞれテーブル数150〜200程度 ◦ いずれのシステムも10年前の技術スタック(独自Java FW、ViewはXSLT) 6
  5. 8.
  6. 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
  7. 13.
  8. 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
  9. 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なフィールドの方 に付与されている
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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 - コード例
  19. 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
  20. 34.

    KotlinTest • https://github.com/kotlintest/kotlintest • 最新ver: 3.1.9 • Kotlinで書かれたBDDテスティングフレームワーク • 様々な書き方を選べる

    • アサーションも含んでいる。JUnitをテスティングフレームワークとして使 い、アサーションにKotlinTestを使うという手もある • Kotlin標準のkotlin.testシリーズとは別物 34
  21. 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 - コード例
  22. 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
  23. 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
  24. 49.

    Mockito - 対処法1: 愚直にopen open class ApiClient { open fun

    foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) • モックする対象のクラス、メソッドを手動でopenする 49
  25. 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
  26. 52.
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 58.
  33. 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
  34. 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
  35. 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
  36. 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 - コード例
  37. 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 - コード例
  38. 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 - コード例
  39. 74.

    2部 How do we test Server-side Kotlin • 実際のプロジェクトでどのようにテストを書いていたか紹介します ◦

    テスト対象のアプリケーションについて ◦ テスト戦略について ◦ アプリケーションサービス、JSON APIのテストについてのTips 74
  40. 75.

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

    新アプリケーションはオニオンアーキテクチャをベースにした構成 ◦ プレゼンテーション層 ◦ アプリケーション層 ◦ ドメイン層 ◦ インフラストラクチャ層 75
  41. 78.

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

    • application ◦ アプリケーションサービス(ユースケース) • infrastructure ◦ DBアクセス、他サービスとのREST API連携など
  42. 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
  43. 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
  44. 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
  45. 84.

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

    基本的にこの層を厚めに書くことで仕様の正しさを担保でき、ドメイ ン層のリファクタリングもしやすくする • JSON APIに対するテスト ◦ レスポンスの正しさ をテスト ◦ 意図したユーザーのみがアクセスできることをテスト 84
  46. 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
  47. 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
  48. 90.

    アプリケーションサービスのテスト方針 • アプリケーションサービスの結合テスト ◦ DBアクセス => モックにしない • テストフレームワークはJUnit4。アサーションはAssertJ ◦

    当初はKotlinTestを使っていたが、Inttelijで実行したときに差 分をInttelijがいい感じにしてくれないので、AssertJに切り替 え • 戻り値をテストするのはもちろん、場合によってはDB変更もテスト する • テストでDBをどのように扱ったかをお話します。 90
  49. 91.

    DBアクセスはモックにしなかった • アプリケーションの構造上、各Infrastructureレイヤーからドメインオブジェク トへのマッピングが必要になるので、少なくともその部分が例外にならない ことはテストしておきたい • 特にKotlinの場合 Null / Not

    Nullが厳密なので、DB上 Null を取りうるカラ ムの値を Not Null のプロパティにマッピングするようなコードがあると簡単 に実行時例外になってしまう • まだデータアクセスライブラリ(Doma2)によるSQL実行が最低限例外なく実 行できることも確認したい • マッピングのテストをかいた上でRepositoryをモックにする方向もあったが、 DbSetupを用いるとDB上のデータの生成が容易だったのでモックにしない 選択をした 91
  50. 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
  51. 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
  52. 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
  53. 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
  54. 103.
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 116.

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

    テストフィクスチャ用のクラスでNull / Not Nullを区別できる • テストフィクスチャ用のクラスはKotlinのdata classになっているた め、copyメソッドを用いて「少しだけパラメータが異なる似たようなフィ クスチャ」を簡単に生成できる 116
  61. 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
  62. 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
  63. 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
  64. 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
  65. 122.

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

    argumentsの形式でコンストラクタの呼び出しを補完してくれる • Kotlin標準のクラスならデフォルト値もついでに与えてくれる 122
  66. 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
  67. 128.

    3部 Our test tools for kotlinのまとめ • 以下のようなツールで開発の効率化を行いました ◦ Factlin

    DBのスキーマからDBSetupのコードを自動生成 ◦ 大量のプロパティを持つクラスの生成にkotlin-fill-class • KotlinはJavaの資産が使えるとはいえ、まだまだこういったKotlin用のツー ルが少ない印象なので今がチャンス!かもしれません。 128