Share
Embed
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
Hidenori Maehara
August 25, 2018
Transcript
129 slides
-
How to Test Server-side Kotlin #kotlinfest エムスリー株式会社 前原 秀徳 @maeharin
エムスリー株式会社 鈴木 健太 @suusan2go 2018/8/25 Kotlin Fest 2018 1 -
自己紹介: 前原 秀徳 • GitHub / Twitterは @maeharin • 2014年5月にエムスリーに入社
• エンジニアチームリーダー、グループ会社の取締役 を歴任後、2017年5月にサーバーサイドKotlinを用 いたレガシーシステムのリニューアルプロジェクトを 立ち上げ • 自慢はブログ記事がはてなブックマーク1200を超え たこと 2 -
自己紹介: 鈴木 健太 • GitHub / Twitterは @suusan2go • 2017年11月に入社して、リニューアルプロジェクトに
参戦 • 前職では主にRails触ってました • リニューアルのプロジェクトでは、KotlinでAPIサーバ 書きつつ、フロントエンドはNuxt.js使って実装みたい なことを担当 • 自慢は娘(2歳)が可愛いことです。 3 -
お話する内容 • 10年前のレガシーシステムをサーバサイドKotlinでリプレイスしました。 その中で培ったKotlinのテストに関する話をします。 • リプレイスしたレガシーシステムは2つ ◦ 1. 医師のキャリア支援システム ◦
2. 薬剤師のキャリア支援システム ◦ 事業規模感: 年間売上合わせて100億円超 ◦ システムの規模感: それぞれテーブル数150〜200程度 ◦ いずれのシステムも10年前の技術スタック(独自Java FW、ViewはXSLT) 6 -
テストのあるシステムへ • APIサーバをKotlin + Spring Boot、フロントをRailsとVue.js(Nuxt.js)で リプレイス ◦ テストのあるシステムへ ◦
2017年5月から開始。リリースはほぼ完了 ◦ 約1500ケースのテストをKotlinで書いてきた 8 -
お話する内容 • 1部: Server-Side Kotlin testing libraries(前原) ◦ サーバーサイドKotlin開発時に有用な主要テストライブラリを紹介 ◦
Kotlinで使う際のTipsやハマりポイントも紹介 • 2部: How do we test Server-Side Kotlin(鈴木) ◦ 実際のプロジェクトでどのようにテストを書いていたかを紹介 • 3部: Our test tools for Kotlin(鈴木) ◦ テストのために発表者が作ったツールを紹介 11 -
お話する内容 今回の発表の対象範囲 サーバーサイドKotlinの単体テス ト、DBに接続したインテグレーション テストについてお話します。 E2Eテストの話やAndroidに特化し たテストの話は行いません。 12 アジャイルテスト -高品質を追求するアジャイルチームにおけるテストの視点
- 増田 聡 氏 P8より引用 https://www.slideshare.net/satoshimasuda/ss-3241717 アジャイルテストの4象限 -
1部 Server-Side Kotlin testing libraries • サーバーサイドKotlin開発時に有用な主要テストライブラリを紹介 • Kotlinで使う際のTipsやハマりポイントも紹介 •
ライブラリ選定の際の参考になれば嬉しいです 13 ※ 各ライブラリの最新verは2018/8/24時点の情報 -
JUnit4 - Kotlinでも問題なく使用することができる • @Testなどのアノテーション含め、Kotlinでも問題なく使用可能 • 一部アノテーションはKotlinで使う時少しだけ留意点あり(後述) class SampleTest {
@Test fun `1 + 2 は 3`() { val actual = sum(1, 2) assertEquals(3, actual) } 18 -
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 -
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なフィールドの方 に付与されている -
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 -
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 -
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 -
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 -
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 -
@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 -
JUnit4 - KotlinでもJUnit4は問題なく使える • Javaとの相互運用でハマったらIntellij IDEAでShow Kotlin Bytecodeするとヒ ントがあるかも •
Tools > Kotlin > Show Kotlin Bytecode 27 -
JUnit5 • https://github.com/junit-team/junit5 • 最新ver: 5.2.0 • JUnit4の後継だが、JUnit4とは全く異なるフレームワーク ◦ Lambda式対応。JUnit4のRuleやテストランナーの廃止など
◦ JUnit Vintageを使えばJUnit4のテストケースの実行も可能 28 -
// 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 -
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 -
Spek • https://github.com/spekframework/spek • 現在2.x準備中 ◦ 1系 最新ver: 1.2.0 ◦
2系 最新ver: 2.0.0-alpha.1 • Kotlinで書かれたテスティングフレームワーク。BDDスタイル • JetBrainsの中の人が作っていた • アサーションは含まないので、好きなアサーションライブラリと組み合わ せて使う 31 -
class SampleTest: Spek({ describe("userが0の時") { val repo = UserMemoryRepository() it("ユーザー数は0")
{ assertEquals(0, repo.count()) } it("存在しないuserを取得しようとするとnull") { assertNull(repo.findById(1)) } } 32 Spek - コード例 -
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 -
KotlinTest • https://github.com/kotlintest/kotlintest • 最新ver: 3.1.9 • Kotlinで書かれたBDDテスティングフレームワーク • 様々な書き方を選べる
• アサーションも含んでいる。JUnitをテスティングフレームワークとして使 い、アサーションにKotlinTestを使うという手もある • Kotlin標準のkotlin.testシリーズとは別物 34 -
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 - コード例 -
現在のIntellij IDEAでは クラスごとにテスト実行は できるが、テストケースごとには実 行できない模様 ※ 以下のverで確認 ・Intellij IDEA ULTIMATE
2018.2 ・KotlinTest 3.1.9 37 KotlinTest - Intellij IDEAからのテスト実行 -
その他のテスティングフレームワーク • TestNG ◦ https://github.com/cbeust/testng ◦ JUnit4と同じく歴史のあるテスティングフレームワーク • Spock ◦
https://github.com/spockframework/spock ◦ groovy製 ◦ power-assertが一つの特徴 38 -
Hamcrest • 基本的なアサーションはJUnit4に組み込まれている • 応用的なアサーションはhamcrest-libraryを別途インストール • Kotlinで使う場合はisが予約語なのでエスケープする必要あり assertThat(a, `is`(b)) //
もしくは import org.hamcrest.CoreMatchers.`is` as Is assertThat(a, Is(b)) 41 -
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 -
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 -
Mockito • https://github.com/mockito/mockito • 最新ver: 2.21.3 • Javaのモックライブラリのデファクトスタンダード • Kotlinで使う際のハマりポイント2つ
◦ 1,finalクラス/メソッドをモックできない問題 ◦ 2,Mockito.any()問題 47 -
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 -
Mockito - 対処法1: 愚直にopen open class ApiClient { open fun
foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) • モックする対象のクラス、メソッドを手動でopenする 49 -
Mockito - 対処法2: インターフェースを使う interface ApiClient { fun foo(): String
} class ApiClientImpl: ApiClient { override fun foo(): String { return "api response" } } val mock = mock(ApiClient::class.java) • テスト対象クラスにインターフェースを実装させ、インターフェースをモッ クする 50 -
Mockito - 対処法3: allopenプラグイン • Kotlinのallopenプラグインを使い、モック対象をopenする • Springの場合、kotlin-springプラグインを使ってSpringのBeanはopen しているケースがほとんどだと思うので、あまり意識することは無いかも しれない
◦ https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support 51 -
// 独自アノテーションを作る package com.maeharin annotation class OpenForMock // モック対象のクラスに独自アノテーションを付与 @com.maeharin.OpenForMock
class ApiClient { fun foo(config: ApiConfig): String { return "real" } } Mockito - 対処法3: allopenプラグイン 52 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
import com.nhaarman.mockitokotlin2.* // モック作成 val mock: ApiClient = mock {
on { foo(any()) } doReturn "dummy" } // モックを注入して実行 SampleService(mock).exec(ApiConfig()) // モックのメソッドが呼ばれたことを検証 verify(mock).foo(any()) Mockito-Kotlin - コード例 60 -
MockK • https://github.com/mockk/mockk • Kotlin製のモックライブラリ • 最新ver: 1.8.6 • デフォルトでfinalクラス/メソッドをモックできる
• mockkのany()を使えばMockito.any()問題は起きない 61 -
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 -
DBUnit • http://dbunit.sourceforge.net/ • 最新ver: 2.6.0 • XML、CSVなどからテストデータをセットアップできる • データベースの状態を検証することもできる
• 2002年にver1.0がリリースされた歴史あるJavaライブラリ 65 -
<!-- 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 - コード例 -
@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 - コード例 -
DbSetup • https://github.com/Ninja-Squad/DbSetup • 最新ver: 2.1.0 • xmlなどの外部ファイルではなく「コード」でテストデータを生成できる Javaライブラリ •
Kotlin用のDSLも提供してくれている(DbSetup-kotlin) 69 -
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 - コード例 -
2部 How do we test Server-side Kotlin • 実際のプロジェクトでどのようにテストを書いていたか紹介します ◦
テスト対象のアプリケーションについて ◦ テスト戦略について ◦ アプリケーションサービス、JSON APIのテストについてのTips 74 -
テスト対象のアプリケーション構成についておさらい • SpringBoot + Kotlin で実装されたAPIサーバ • 10年弱運用されてきたアプリケーションのリニューアル!(Java独 自FW) •
新アプリケーションはオニオンアーキテクチャをベースにした構成 ◦ プレゼンテーション層 ◦ アプリケーション層 ◦ ドメイン層 ◦ インフラストラクチャ層 75 -
Kotlin + Spring Boot API サーバ REST APIでやりとり もしくは バックエンドのAPIとしてSpring
Boot + Kotlin 76 -
パッケージ構成 78 • presentation ◦ WEBのエンドポイント(controllerとか) • domain ◦ ドメインエンティティ、バリューオブジェクト
• application ◦ アプリケーションサービス(ユースケース) • infrastructure ◦ DBアクセス、他サービスとのREST API連携など -
プレゼンテーションの実装イメージ @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 -
アプリケーションサービスの実装イメージ @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 -
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 -
pros/consを踏まえたテスト方針 • テスト対象は基本的にアプリケーションサービスより上の層 ◦ ※ドメイン層はロジックの複雑なものだけテスト • アプリケーションサービスに対するテスト ◦ アプリケーションとして要求される仕様を満たしているかをテスト ◦
基本的にこの層を厚めに書くことで仕様の正しさを担保でき、ドメイ ン層のリファクタリングもしやすくする • JSON APIに対するテスト ◦ レスポンスの正しさ をテスト ◦ 意図したユーザーのみがアクセスできることをテスト 84 -
Presentation Application Domain Infrastructure アプリケーションサービスのカバー範囲 モックしない PostgreSQL 外部API ElasticSearch 開発者毎に
Docker モック アプリケーションサービスのテスト 87 -
アプリケーションサービスのテストコード // アプリケーションサービスのテストコードサンプル 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 -
アプリケーションサービスのテストコード @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 -
アプリケーションサービスのテスト方針 • アプリケーションサービスの結合テスト ◦ DBアクセス => モックにしない • テストフレームワークはJUnit4。アサーションはAssertJ ◦
当初はKotlinTestを使っていたが、Inttelijで実行したときに差 分をInttelijがいい感じにしてくれないので、AssertJに切り替 え • 戻り値をテストするのはもちろん、場合によってはDB変更もテスト する • テストでDBをどのように扱ったかをお話します。 90 -
DBアクセスはモックにしなかった • アプリケーションの構造上、各Infrastructureレイヤーからドメインオブジェク トへのマッピングが必要になるので、少なくともその部分が例外にならない ことはテストしておきたい • 特にKotlinの場合 Null / Not
Nullが厳密なので、DB上 Null を取りうるカラ ムの値を Not Null のプロパティにマッピングするようなコードがあると簡単 に実行時例外になってしまう • まだデータアクセスライブラリ(Doma2)によるSQL実行が最低限例外なく実 行できることも確認したい • マッピングのテストをかいた上でRepositoryをモックにする方向もあったが、 DbSetupを用いるとDB上のデータの生成が容易だったのでモックにしない 選択をした 91 -
// 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 -
DBがどのように変更されたかをテストする • DBが意図した状態に変更されたことをチェックしたい ◦ 特にDBとドメインエンティティのマッピングが単純ではない場 合や、意図しないデータを更新していないか、意図した件数 更新しているかなどを厳密にチェックしたい場合 • AssertJ-DBを用いてDBの状態をテストした •
AsserJ-DBはDBの変更をAssertJのようなシンタックスでテストで きるライブラリ 94 -
@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 -
JSON APIのテストがカバーする範囲(おさらい) Presentation Application Domain Infrastructure モックしない PostgreSQL 外部API ElasticSearch
開発者毎に Docker モック JSON APIのテスト 98 -
テストコード @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 -
拡張関数の開設 // 事前にログイン用の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 -
// 渡されたオブジェクトを JSON化してリクエストボディに詰める fun MockHttpServletRequestBuilder.withJson( object: Object ): MockHttpServletRequestBuilder {
this.contentType(APPLICATION_JSON_UTF_8) this.content(ObjectMapper().writeValueAsString(object) return this } 103 -
拡張関数の開設 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 -
// 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 -
• プロパティが数十個あってネストしているAPIの「期待値」を手で書いて いくのは骨が折れる • 最初のテスト実行時に正常なAPIレスポンスの期待値をファイルに保存 し、このファイルを元に期待するJSONフォーマットを記述した • このJSONファイルを期待値としてJsonUnitでアサートするようにする と、スピーディーにテストを書くことができた •
また、これにより意図せずAPIの形が変わった場合にもテストで気がつ けるようになった 期待するJSONをどのように用意するか 107 -
@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 -
例:こんなクラスのカバレッジを考える 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 -
How do we test Servier-Side Kotlinのまとめ • アプリケーションサービスより上の層を中心にテストしてます • アプリケーションサービスの結合テストのTips
◦ DBSetupでのデータセットアップ ◦ AssertJ-DBを使ったデータベース状態のテスト • JSON APIでのテストのTips ◦ 拡張関数でテスト用のリクエストを宣言的に記述できる ◦ JSONUnit & JSONファイル生成でJSONのテストを容易に • テストのカバレッジはjacocoで。 0.8.2からはData Classもカバレッジが正しく取得できるように! 114 -
3部 Our test tools for Kotlin • テストのために発表者が作ったツールたちの紹介 ◦ Factlin
◦ kotlin-fill-class 115 -
Factlin (by @maeharin) • DbSetupのシンタックスは平易とはいえ、カラムの多いテーブルに対 するセットアップコードを一から書くのは大変 • Factlinを使うことで、既存のデータベーススキーマから、DbSetup用 のコードを生成できる •
テストフィクスチャ用のクラスでNull / Not Nullを区別できる • テストフィクスチャ用のクラスはKotlinのdata classになっているた め、copyメソッドを用いて「少しだけパラメータが異なる似たようなフィ クスチャ」を簡単に生成できる 116 -
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 -
// 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 -
// 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 -
// 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 -
Kotlin Fill Class (by @suusan2go) • コンストラクタを書くのを少しだけ楽にしてくれるIntellij Plugin • Named
argumentsの形式でコンストラクタの呼び出しを補完してくれる • Kotlin標準のクラスならデフォルト値もついでに与えてくれる 122 -
たとえばこんなクラスをインスタンス化するコード書くの辛くないですか 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 -
Kotlin Fill Classの実装について • Inttelijプラグインの作り方自体は今回の発表のスコープから外れるので以下を ご参照ください!!! • そしてダウンロードしてください!!! • まだ機能は荒削りなのでpulll-requestお待ちしてます!
• Kotlinでコンストラクタをシュッと書くためにIntellijのプラグインを作った 127 -
3部 Our test tools for kotlinのまとめ • 以下のようなツールで開発の効率化を行いました ◦ Factlin
DBのスキーマからDBSetupのコードを自動生成 ◦ 大量のプロパティを持つクラスの生成にkotlin-fill-class • KotlinはJavaの資産が使えるとはいえ、まだまだこういったKotlin用のツー ルが少ない印象なので今がチャンス!かもしれません。 128