🔥

[調査記録] roborazziが自動生成したPreviewテストが失敗する

2024/11/15に公開

諸注意

業務で得た知見なので、記事のなかで示すコードは改変しています

./gradlew recordRoborazzi が正常終了しない!!

https://zenn.dev/karabiner_inc/articles/4d939b478c5f40

前回、roborazziを導入してはまった点についての記事を書きました。実はその裏でもっとはまっていたことがありました。
それは、./gradlew recordRoborazziでスクリーンショットを撮ろうとしてもタスクが正常終了せずに、画像が出力されないというものです。

経緯

  1. とりあえず、roborazziが導入できるか試してみる
    • できた!
  2. 毎週実施しているライブラリアップデートによってfirebase-bomのバージョンが上がる(33.3.0 → 33.4.0)
  3. ベースブランチを更新して、再度確認
    • なぜか失敗するようになった。。。

なにがつらいってエラーログから得られる情報が皆無なんですよね。。

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:testDemoDebugUnitTest'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:130)
        at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:293)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:128)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:459)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:376)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
Caused by: org.gradle.process.internal.ExecException: Process 'Gradle Test Executor 19' finished with non-zero exit value 1
This problem might be caused by incorrect test process configuration.
For more on test execution, please refer to https://docs.gradle.org/8.10.2/userguide/java_testing.html#sec:test_execution in the Gradle documentation.
        at org.gradle.api.internal.tasks.testing.worker.ForkingTestClassProcessor.stop(ForkingTestClassProcessor.java:161)
        at org.gradle.api.internal.tasks.testing.processors.RestartEveryNTestClassProcessor.endBatch(RestartEveryNTestClassProcessor.java:77)
        at org.gradle.api.internal.tasks.testing.processors.RestartEveryNTestClassProcessor.stop(RestartEveryNTestClassProcessor.java:62)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
        at org.gradle.internal.dispatch.FailureHandlingDispatch.dispatch(FailureHandlingDispatch.java:30)
        at org.gradle.internal.dispatch.AsyncDispatch.dispatchMessages(AsyncDispatch.java:87)
        at org.gradle.internal.dispatch.AsyncDispatch.access$000(AsyncDispatch.java:36)
        at org.gradle.internal.dispatch.AsyncDispatch$1.run(AsyncDispatch.java:71)
        at org.gradle.internal.concurrent.InterruptibleRunnable.run(InterruptibleRunnable.java:42)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
        at org.gradle.internal.operations.CurrentBuildOperationPreservingRunnable.run(CurrentBuildOperationPreservingRunnable.java:51)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)

そして、この状態で単体テストを実行するとそれもなぜか失敗するという状況。。

暫定対応

CIで実行している単体テストも失敗している状況は看過できないので、一旦roborazzi用のビルド設定はroborazzi.gradleというファイルに切り出して、実行したいときに適用するという運用にしました

// roborazzi.gradle
apply plugin: libs.plugins.roborazzi.get().pluginId

android {
    testOptions {
        unitTests {
            includeAndroidResources = true
            all {
                it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware"
            }
        }
    }
}

roborazzi.generateComposePreviewRobolectricTests.enable.set(true)
roborazzi.generateComposePreviewRobolectricTests.packages.set(["your.package.name"])
roborazzi.generateComposePreviewRobolectricTests.includePrivatePreviews.set(true)

dependencies {
    testImplementation(libs.junit)
    testImplementation(libs.robolectric)
    testImplementation(libs.roborazzi)
    testImplementation(libs.roborazzi.compose)
    testImplementation(libs.roborazzi.rule)
    testImplementation(libs.roborazziComposePreviewScannerSupport)
    testImplementation(libs.composablePreviewScannerAndroid)
}
// build.gradle
// ...
// apply from: "path/to/roborazzi.gradle"

roborazziを使う手順

  1. firebase-bomのバージョンを33.3.0に下げる
  2. roborazzi.gradleのコメントアウトを外す
  3. git switch develop (ベースブランチがdevelpなのでそうしています)
  4. ./gradlew recordRoborazzi
  5. git switch feature/hoge
  6. ./gradlew compareRoborazzi
  7. fd _compare --no-ignore --extension png -X mv {} . (_compareで終わるpngファイルを抽出)
    • fdっていうfindの代替コマンドを使用しているので、findを使う場合は読み替えてください

どうやらapp-moduleだけが失敗するらしい

その後も時間があるときに原因を調査していました。

https://x.com/maruisannsimai/status/1855269025905012803

recordRoborazziではなくapp:recordRoborazzihoge:recordRoborazziといった、モジュール単位での実行を見たところ、app-moduleだけが失敗することが分かりました。

Firebase関連の実装を消したり、Fakeに差し替えることで成功するようになった🎉

Firebaseが悪さをしていることは薄々気づいていたので、Firebase関連の実装をコメントアウトしたり、mockに差し替えたりしました

// こういうのとか
// FirebaseApp.initializeApp(this)

// こういうやつ
// FirebaseCrashlytics.getInstance() 
//      .setCrashlyticsCollectionEnabled(true) 
//
// RxJavaPlugins.setErrorHandler { 
//     FirebaseCrashlytics.getInstance() 
//         .recordException(it) 
// }
// DIしている箇所はmockに変える
@Provides
@Singleton
fun provideFirebaseRemoteConfig(
    // ...
): FirebaseRemoteConfig {
    return Mockito.mock()
}

このようにしたところ、app:recordRoborazziも動くようになりました。
ということでFirebaseがテストの実行を妨げていて、モックなりフェイク実装なりで差し替える必要があることが分かりました。

roborazziがPreviewを元にテストを自動生成しているので、差し替え不可能。。

さぁ、DI設定を差し替えればよいことは分かりましたが、次の問題が発生しました。
それはDI設定を差し替えられないということです。
自分の手でテストを書いている場合は、@TestInstallInとか@HiltAndroidTestを使って差し替えればよいだけですが、今回僕はroborazziの便利な機能であるプレビューをベースにテストを自動生成する機能を使っています。
https://github.com/takahirom/roborazzi?tab=readme-ov-file#generate-compose-preview-screenshot-tests
自動生成されたテストに対してこちらができることはないので差し替えることは不可能だということが分かりました。
ということでapp-moduleについては自動生成をつかったテストは諦めることにしました。

つぎはどうするのか

  • app-module以外: roborazzi(自動)を適用する
  • app-module: 必要があれば手動でテストを書く
  • 現状画面レベルのComposable関数がapp-moduleにいるので、適当なモジュールに移動させる
GitHubで編集を提案
カラビナテクノロジー デベロッパーブログ

Discussion