$30 off During Our Annual Pro Sale. View Details »

テストコードを書きながらCompose Multiplatformを乗りこなす

テストコードを書きながらCompose Multiplatformを乗りこなす

subroh_0508

May 13, 2024
Tweet

More Decks by subroh_0508

Other Decks in Technology

Transcript

  1. 2 プロダクト本部 開発部 DevHR 坂上 晴信 Harunobu Sakaue 【¥431,893】にしこりさぶろ〜 @subroh_0508

    🎤経歴 1995年生まれ。東京の離島・伊豆大島出身。 メインの技術スタックはKotlin・Android・Rails・React。 東京高専情報工学科を卒業後、2016年4月に株式会社 TOKIUMに新卒入社。6年半に渡りAndroid・Webの領域で プレイングマネージャーとして経験を積み、2023年1月から DevHRとしてエンジニア採用・組織づくりにフルコミットで 携わる。 リアルのすがた インターネットのすがた
  2. TOKIUMの志 未 来 へ つ な が る 時 を

    生 む TOKIUMは、より良い世界を志す人の「未来へつながる時」を生むために存在します。 それは、誰かのために、調べ、考え、挑戦するための時間です。 TOKIUMは、時間のインフラでありたい。 最適なテクノロジーと、常識にとらわれない自由な発想と、泥臭さもいとわない行動力で、 人と事業を未来へ向けてもっと加速させていきたいのです。 自己紹介 / 会社紹介 3 Android/Webエンジニアとして約7年 人事職にロールチェンジしてから、現在2年目
  3. 話すこと 4 ✓ Compose MultiplatformのUIテストについて、実行環境の設定方法を共有 ➔ Android (for Instrumented Test)・iOS・Desktopの各ビルドターゲット用に

    Composable関数の挙動を検証するテストコードを実行する方法の紹介 ➔ Android用のCompose Multiplatformのテストコードについて ローカルJVM上で実行する方法の紹介 複数ターゲットに向けたビルドが前提のCompose Multiplatformにおいて テストコードは品質の維持・向上に必要不可欠👊 今回共有するナレッジによって、Compose Multiplatformを使って Production Readyなアプリの開発に挑戦する開発者を少しでも増やせれば😊
  4. 前提知識 5 🤔 Compose Multiplatformとは? ➔ Jetpack ComposeのKotlin Multiplatform対応版 ➔

    iOS・Desktop・WebアプリのUIをJetpack Composeと(ほぼ)同じAPIで 宣言的に実装することができるフレームワーク 公式サイト: Compose Multiplatform UI フレームワーク | JetBrains Desktop向けは既に安定版に到達! iOS向けも、実はα版までリリースされている!
  5. 前提知識 6 🤔 Compose Multiplatform、実際どこまでできるの? ➔ 想像以上になんでもできます!👍 現在、本番リリースに向けて開発中の Mastodonクライアントアプリ (左:

    iOS / 右: Android) 💪実装済の機能 - OAuth認証によるログイン - 複数アカウントの認証情報保持・切り替え - トゥートの閲覧・投稿 - ストリーミングの購読 (→タイムラインの自動更新) - 画像・動画データのプレビュー - ファイルピッカーによる メディアファイルの選択・プレビュー 🔧内部実装 - Dependency Injection導入済み - ドメイン・UI層双方に単体テスト導入済み こんなにできるなら自分もやってみたい!
  6. 前提知識 7 😇 Android以外のビルドターゲットに関する情報が少ない ➔ 触っている人が少ないため、情報も少なくなりがち ➔ 特にiOSへの対応時、慣れていないiOS・Swift・Obj-Cの知識が要求される 😇 動作検証が大変

    ➔ シンプルに動作検証対象が多く、見た目とロジックの確認に労力がかかる ➔ @Preview アノテーションの使用に制限がある ※ commonMain 以下に実装されたComposable関数の @Preview は、JetBrains Fleetでしか動作しない(v1.6.2時点) ただ、現状はまだまだエッジの効いた技術 使いこなす上でのつらみも複数存在🫠
  7. 前提知識 8 😇 Android以外のビルドターゲットに関する情報が少ない ➔ 触っている人が少ないため、情報も少なくなりがち ➔ 特にiOSへの対応時、慣れていないiOS・Swift・Obj-Cの知識が要求される 😇 動作検証が大変

    ➔ シンプルに動作検証対象が多く、見た目とロジックの確認に労力がかかる ➔ @Preview アノテーションの使用に制限がある ※ commonMain 以下に実装されたComposable関数の @Preview は、JetBrains Fleetでしか動作しない(v1.6.2時点) ただ、現状はまだまだエッジの効いた技術 使いこなす上でのつらみも複数存在🫠 アプリ実装の実績をたくさん 積み重ねて、外部に伝える💪 ※後日話す UIテストの実行環境を整え、一度テストコード化した項目は 自動で検証できるようにする💪※今日話す
  8. Compose MultiplatformのUIテスト / 概要 9 👀 テストコードの外観 class ExampleTest {

    @OptIn(ExperimentalTestApi::class) @Test fun myTest() = runComposeUiTest { setContent { var text by remember { mutableStateOf("Hello") } Text( text = text, modifier = Modifier.testTag("text") ) Button( onClick = { text = "Compose" }, modifier = Modifier.testTag("button") ) { Text("Click me") } } onNodeWithTag("text").assertTextEquals("Hello") onNodeWithTag("button").performClick() onNodeWithTag("text").assertTextEquals("Compose") } } 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation
  9. Compose MultiplatformのUIテスト / 概要 10 👀 テストコードの外観 class ExampleTest {

    @OptIn(ExperimentalTestApi::class) @Test fun myTest() = runComposeUiTest { setContent { var text by remember { mutableStateOf("Hello") } Text( text = text, modifier = Modifier.testTag("text") ) Button( onClick = { text = "Compose" }, modifier = Modifier.testTag("button") ) { Text("Click me") } } onNodeWithTag("text").assertTextEquals("Hello") onNodeWithTag("button").performClick() onNodeWithTag("text").assertTextEquals("Compose") } } テスト対象のUI アサーションの実行 ボタンクリックによって テキストが変化するUIの検証 Finder・アサーション アクション・マッチャー 全てJetpack Composeと同じAPIで 利用可能!激アツ!🔥 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation
  10. Compose MultiplatformのUIテスト / 概要 11 🏃 テストコードを各ターゲットごとに実行してみる ➔ Android: ./gradlew

    :connectedAndroidTest a ➔ iOS: ./gradlew :iosSimulatorArm64Test a ➔ ➔ Desktop: ./gradlew :desktopTest a 単一のテストクラスで 3つのターゲットを対象とした 動作検証ができる!激アツ!🔥
  11. Compose MultiplatformのUIテスト / 環境設定 13 🐘 UIテストの実行環境を整える(2/3) ➔ Gradleファイルを編集し、依存関係を追加 kotlin

    { //... sourceSets { val desktopTest by getting commonTest.dependencies { implementation(kotlin("test")) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.uiTest) } desktopTest.dependencies { implementation(compose.desktop.uiTestJUnit4) implementation(compose.desktop.currentOs) } } } tasks.named<Test>("desktopTest") { useJUnitPlatform() } ${moduleName}/build.gradle.kts 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation
  12. Compose MultiplatformのUIテスト / 環境設定 14 🐘 UIテストの実行環境を整える(2/3) ➔ Gradleファイルを編集し、依存関係を追加 kotlin

    { //... sourceSets { val desktopTest by getting commonTest.dependencies { implementation(kotlin("test")) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.uiTest) } desktopTest.dependencies { implementation(compose.desktop.uiTestJUnit4) implementation(compose.desktop.currentOs) } } } tasks.named<Test>("desktopTest") { useJUnitPlatform() } ${moduleName}/build.gradle.kts 全ターゲット共通の依存関係 Desktop向けに必要な依存関係・設定 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation
  13. Compose MultiplatformのUIテスト / 環境設定 15 🐘 UIテストの実行環境を整える(3/3) ➔ Gradleファイルを編集し、Android向けの設定を追加 kotlin

    { //... androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { sourceSetTree.set(KotlinSourceSetTree.test) dependencies { implementation("androidx.compose.ui:ui-test-junit4-android:1.5.4") debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4") } } } } android { defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } ${moduleName}/build.gradle.kts 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation
  14. Compose MultiplatformのUIテスト / 環境設定 16 🐘 UIテストの実行環境を整える(3/3) ➔ Gradleファイルを編集し、Android向けの設定を追加 kotlin

    { //... androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { sourceSetTree.set(KotlinSourceSetTree.test) dependencies { implementation("androidx.compose.ui:ui-test-junit4-android:1.5.4") debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4") } } } } android { defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } ${moduleName}/build.gradle.kts Android向けに必要な依存関係・設定 Instrumented Testの実行に必要な設定 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation
  15. Compose MultiplatformのUIテスト / 環境設定 17 🐘 UIテストの実行環境を整える(3/3) ➔ Gradleファイルを編集し、Android向けの設定を追加 kotlin

    { //... androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { sourceSetTree.set(KotlinSourceSetTree.test) dependencies { implementation("androidx.compose.ui:ui-test-junit4-android:1.5.4") debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4") } } } } android { defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } ${moduleName}/build.gradle.kts Android向けに必要な依存関係・設定 Instrumented Testの実行に必要な設定 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation ここまでできたらGradleタスクを実行するだけ!👍 環境設定の仕方は、公式のドキュメントも用意されてるぞ😎 ※この資料では一部の設定を改変しています
  16. Compose MultiplatformのUIテスト / 環境設定 18 🐘 UIテストの実行環境を整える(3/3) ➔ Gradleファイルを編集し、Android向けの設定を追加 kotlin

    { //... androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { sourceSetTree.set(KotlinSourceSetTree.test) dependencies { implementation("androidx.compose.ui:ui-test-junit4-android:1.5.4") debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4") } } } } android { defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } ${moduleName}/build.gradle.kts Android向けに必要な依存関係・設定 Instrumented Testの実行に必要な設定 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation Compose MultiplatformのUIテストが実行できるようになった!🎉 これにて一件落着、Have a nice Kotlin!👋 ちょっと待って…!1つ気になるところが…!
  17. Compose MultiplatformのUIテスト / 環境設定 19 🐘 UIテストの実行環境を整える(3/3) ➔ Gradleファイルを編集し、Android向けの設定を追加 kotlin

    { //... androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { sourceSetTree.set(KotlinSourceSetTree.test) dependencies { implementation("androidx.compose.ui:ui-test-junit4-android:1.5.4") debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4") } } } } android { defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } ${moduleName}/build.gradle.kts Android向けに必要な依存関係・設定 Instrumented Testの実行に必要な設定 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation Compose MultiplatformのUIテストが実行できるようになった!🎉 これにて一件落着、Have a nice Kotlin!👋 どうしてAndroid向けのテストだけ、実機実行なんですか?
  18. Compose MultiplatformのUIテスト / 実行上の制約 21 😭 Android向けのテストは、実機での実行しかできない! ➔ 試しに ./gradlew

    :testDebugUnitTest を実行し、 ローカルJVM上でUIテストを実行してみると… 「 "android.os.Build.FINGERPRINT" がnullになってしまう」と怒られる😢 出典: Testing Compose Multiplatform UI | Kotlin Multiplatform Development Documentation
  19. Compose MultiplatformのUIテスト / 実行上の制約 22 🤔 テストの実機実行のメリット・デメリット ➔ 🙆実際のアプリの実行環境に、より忠実な環境下でテストが実行される ➔

    🙅1回のテスト実行にかかるコストが重い GitHub Actions等のCI/CD環境では、1回の実行でリソースを大量に消費してしまう すなわち、「commitの度に単体テストを実行する」ことが難しくなる あと、CI/CD環境でエミュレーター立ち上げる設定書くのもめんどくさい😇 何としてでも、Android向けのテストを ローカルJVM上で動かしたい…!
  20. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 23 👊 Android向けテストを(なんとかして)ローカルJVM上で動かす ➔ 方針: Android向けテストを実行する時のみ、

    Test RunnerをJUnit4 + Robolectricに差し替えられれば上手くいく(はず) なお、Compose MultiplatformのUIテストは JUnit5上で実行されている模様👀 ※Android向けの @Test アノテーションが JUnit5のアノテーションを参照しているため
  21. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 24 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(1/4) ➔ 必要な依存関係を追加 sourceSets

    { androidMain.dependencies { implementation(libs.androidx.test.junit) implementation(libs.androidx.activity.compose) } desktopMain.dependencies { implementation(kotlin("test-junit5")) implementation(compose.desktop.currentOs) } androidUnitTest.dependencies { implementation(libs.androidx.compose.ui.test.junit4) runtimeOnly(libs.junit.core) runtimeOnly(libs.junit.vintage) runtimeOnly(libs.robolectric) } } ${moduleName}/build.gradle.kts
  22. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 25 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(1/4) ➔ 必要な依存関係を追加 sourceSets

    { androidMain.dependencies { implementation(libs.androidx.test.junit) implementation(libs.androidx.activity.compose) } desktopMain.dependencies { implementation(kotlin("test-junit5")) implementation(compose.desktop.currentOs) } androidUnitTest.dependencies { implementation(libs.androidx.compose.ui.test.junit4) runtimeOnly(libs.junit.core) runtimeOnly(libs.junit.vintage) runtimeOnly(libs.robolectric) } } ${moduleName}/build.gradle.kts この後のアノテーション定義に必要
  23. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 26 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(1/4) ➔ 必要な依存関係を追加 sourceSets

    { androidMain.dependencies { implementation(libs.androidx.test.junit) implementation(libs.androidx.activity.compose) } desktopMain.dependencies { implementation(kotlin("test-junit5")) implementation(compose.desktop.currentOs) } androidUnitTest.dependencies { implementation(libs.androidx.compose.ui.test.junit4) runtimeOnly(libs.junit.core) runtimeOnly(libs.junit.vintage) runtimeOnly(libs.robolectric) } } ${moduleName}/build.gradle.kts Android向けテストの Test Runnerの差し替え + 実行に利用
  24. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 27 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(2/4) ➔ 必要なアノテーションを定義する expect

    abstract class Runner expect class UiTestRunner : Runner expect annotation class RunWith(val value: KClass<out Runner>) expect annotation class ComposeTest() commonMain/Annotations.kt
  25. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 28 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(2/4) ➔ 必要なアノテーションを定義する expect

    abstract class Runner expect class UiTestRunner : Runner expect annotation class RunWith(val value: KClass<out Runner>) expect annotation class ComposeTest() commonMain/Annotations.kt Test Runnerの指定に利用するアノテーション (と付随して必要になるクラス) kotlin.testの @Test アノテーションの 代わりに利用するアノテーション
  26. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 29 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(2/4) ➔ 必要なアノテーションを定義する actual

    typealias Runner = org.junit.runner.Runner actual typealias UiTestRunner = androidx.test.ext.junit.runners.AndroidJUnit4 actual typealias RunWith = org.junit.runner.RunWith actual typealias ComposeTest = org.junit.Test androidMain/Annotations.android.kt
  27. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 30 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(2/4) ➔ 必要なアノテーションを定義する actual

    typealias Runner = org.junit.runner.Runner actual typealias UiTestRunner = androidx.test.ext.junit.runners.AndroidJUnit4 actual typealias RunWith = org.junit.runner.RunWith actual typealias ComposeTest = org.junit.Test AndroidJUnit4 を参照するように定義 JUnit4の @Test アノテーションを参照するように定義 androidMain/Annotations.android.kt
  28. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 31 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(2/4) ➔ 必要なアノテーションを定義する actual

    abstract class Runner actual class UiTestRunner : Runner() actual annotation class RunWith(actual val value: KClass<out Runner>) actual typealias ComposeTest = kotlin.test.Test iosMain/Annotations.ios.kt actual abstract class Runner actual class UiTestRunner : Runner() actual annotation class RunWith(actual val value: KClass<out Runner>) actual typealias ComposeTest = org.junit.jupiter.api.Test desktopMain/Annotations.jvm.kt iOS・Desktopは、以前の通りに動けばOK 空のクラス定義 + kotlin.testの定義への参照
  29. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 33 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(3/4) ➔ テストコードを修正 @RunWith(UiTestRunner::class)

    class ExampleTest { @OptIn(ExperimentalTestApi::class) @ComposeTest fun myTest() = runComposeUiTest { // ... } } commonMain/ExampleTest.kt 追加 @Test → @ComposeTest に修正
  30. Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす 34 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(4/4) ➔ Android(Local): ./gradlew

    :testDebugUnitTest a ➔ iOS: ./gradlew :iosSimulatorArm64Test a ➔ ➔ Desktop: ./gradlew :desktopTest a ローカルJVM上でのテストが 通るように!🎉 他ターゲットのテストも動く!
  31. 35 🐥 Android向けテストを(なんとかして)ローカルJVM上で動かす(4/4) ➔ Android(Local): ./gradlew :testDebugUnitTest a ➔ Android(Instrumented):

    ./gradlew :connectedAndroidTest aa 因みに、実機実行のテストも ちゃんと動作します😉 Compose MultiplatformのUIテスト / Android向けテストをローカルJVM上で動かす
  32. まとめ 36 👍 Compose Multiplatformでは、単一のテストコードで複数のターゲットに対し UIテストを実行することができる ➔ 動作検証が煩雑になりやすいX-Plat開発にとって、非常に強力といえる 👍 各ターゲット単体でできることは、Compose

    Multiplatformを使っていても 大体実現できる ➔ expect / actual 修飾子を上手く扱いながら、各ターゲットの実行環境を 再現する方向性で試行錯誤すると◎ 今回話した内容が網羅されたレポジトリはコチラ → subroh0508/compose-uitest-sample Compose Multiplatformを使ったアプリの開発に、みなさんもチャレンジ!😉
  33. 宣伝 37 Kotlin Fest 2024で登壇します✌ 開催日: 2024/06/22(Sat) 場所: ベルサール渋谷ファースト 05/17までチケットが安い!

    ¥9,000 → ¥5,000 今日話せなかったことを たくさん話します😎 エンジニア積極採用中! Rails / React / AWS Kotlin / Swift 3月に採用サイトを リニューアル! 見に来てもらえると🙏 Thank you for listening!