🤖

[備忘録] roborazzi導入してみた

2024/11/01に公開

案件のアプリケーションにroborazziを導入しようとして、いろいろはまったところがあったので備忘録的な記録をします

ゴール

  • junit5を使って単体テストを書いているプロジェクトにroborazziを導入できるようになる
  • プレビューテストの妨げになるComposable関数が分かるようになる

依存を追加

  // root-level build.gradle.kts
  plugin {
      // ...
+     id("io.github.takahirom.roborazzi") version "1.29.0" apply false
  }
  // module-level build.gradle.kts
  plugin {
      // ...
+     id("io.github.takahirom.roborazzi")
  }
  android {
      // ...
+     testOptions {
+         unitTests {
+             isIncludeAndroidResources = true
+             all {
+                 it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware"
+             }
+         }
+     }
  }
  // module-level build.gradle.kts
  dependencies {
    // ...
+   implementation("junit:junit:4.13.2")
+   implementation("org.robolectric:robolectric:4.13")
+   implementation("io.github.takahirom.roborazzi:roborazzi:1.29.0")
+   implementation("io.github.takahirom.roborazzi:roborazzi-junit-rule:1.29.0")
+   implementation("io.github.takahirom.roborazzi:roborazzi-compose:1.29.0")
+   implementation("io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:1.29.0")
+   implementation("o.github.sergio-sastre.ComposablePreviewScanner:android:0.3.2")
  }

プレビューテスト自動生成のための記述をする

// module-level build.gradle.kts
roborazzi {
    generateComposePreviewRobolectricTests {
        enable = true
        packages = listOf("your.package.name")
        // privateなプレビューを含めたいならincludePrivatePreviewsを利用する
        // includePrivatePreviews = true
    }
}

スクリーンショットの撮影および差分生成

// 比較元となるブランチに移動
git switch develop

// スクリーンショットを撮影
./gradlew recordRoborazziDemoDebug

// 作業ブランチに移動
git switch {branch}

// 差分を作成
./gradlew compareRoborazziDemoDebug

// tips 差分画像のみを抽出
find . -type f -name "_compare" -exec mv {} . \;

はまりポイント1: junit5を利用している場合は、junit-vintage-engineを追加する

roborazziによって生成されるテストはJUnit4ベースだったので、JUnit5を使っている場合はjunit-vintage-engineを追加する必要があります

  implementation(platform("org.junit:junit-bom:5.11.3"))
  // run junit4-based test (roborazzi)
+ testRuntimeOnly("org.junit.jupiter:junit-vintage-engine")
  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
  testImplementation("org.junit.jupiter:junit-jupiter-api")
  testImplementation("org.junit.jupiter:junit-jupiter-params")

はまりポイント2: PreviewにDialogを含めないようにする

Dialogを使ったComposable関数について自動生成されたスクリーンショットテストは失敗します

@Composable
fun HogeDialog(
    // ...
) {
    Dialog(
        onDismissRequest = onDismiss,
    ) {
        // content
    }
}
java.lang.IllegalStateException: Unable to find the image of the target root component. Does the rendering element exist?
    at com.github.takahirom.roborazzi.RoborazziKt.capture(Roborazzi.kt:613)
    at com.github.takahirom.roborazzi.ImageCaptureViewAction.perform(Roborazzi.kt:587)
    at androidx.test.espresso.ViewInteraction$SingleExecutionViewAction.perform(ViewInteraction.java:2)
    at androidx.test.espresso.ViewInteraction.doPerform(ViewInteraction.java:25)
    at androidx.test.espresso.ViewInteraction.-$$Nest$mdoPerform(ViewInteraction.java)
    at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:7)
    at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:1)
    at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    at android.os.Handler.$$robo$$android_os_Handler$handleCallback(Handler.java:942)
    at android.os.Handler.handleCallback(Handler.java)
    at android.os.Handler.$$robo$$android_os_Handler$dispatchMessage(Handler.java:99)
    ...

「レンダリング可能な要素がないんだが?」と怒られてますね
このような場合にはDialog内を切り出して、それのプレビューをテストしてます

Caution: Preview関数名はコンポーネントと同じ名前にしてはいけない

Preview関数名がコンポーネントと同一であればテストが失敗します

@Composable
fun HogeScreen(
    // ...
) {
    // ...
}

@Preview
@Composable
fun HogeScreen() {
    MaterialTheme {
        Surface {
            HogeScreen(
                // ...
            )
        }
    }
}
java.lang.IllegalArgumentException: wrong number of arguments: 2 expected: 5
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.checkArgumentCount(Unknown Source)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
    at sergio.sastre.composable.preview.scanner.core.preview.ComposablePreviewInvocationHandler.invoke(ComposablePreviewInvocationHandler.kt:30)
    at jdk.proxy4/jdk.proxy4.$Proxy58.invoke(Unknown Source)
    at sergio.sastre.composable.preview.scanner.core.preview.ProvideComposablePreview$invoke$1.invoke(ProvideComposablePreview.kt)
    at com.github.takahirom.roborazzi.RoborazziPreviewScannerSupportKt$captureRoboImage$1.invoke(RoborazziPreviewScannerSupport.kt:18)
    at com.github.takahirom.roborazzi.RoborazziPreviewScannerSupportKt$captureRoboImage$1.invoke(RoborazziPreviewScannerSupport.kt:17)
    at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
    at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
    at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:441)
    ...

Tips: メモリは潤沢に用意したほうがよさそう

メモリ不足でテストが失敗しやすいのでヒープサイズを広く確保しておくことをオススメします

android {
    // ...
    testOptions {
        unitTests {
            isIncludeAndroidResources = true
            all {
                maxHeapSize = "4096m"
                it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware"
            }
        }
    }
}
GitHubで編集を提案
カラビナテクノロジー デベロッパーブログ

Discussion