[調査記録] roborazziが自動生成したPreviewテストが失敗する
諸注意
業務で得た知見なので、記事のなかで示すコードは改変しています
./gradlew recordRoborazzi
が正常終了しない!!
前回、roborazziを導入してはまった点についての記事を書きました。実はその裏でもっとはまっていたことがありました。
それは、./gradlew recordRoborazzi
でスクリーンショットを撮ろうとしてもタスクが正常終了せずに、画像が出力されないというものです。
経緯
- とりあえず、roborazziが導入できるか試してみる
- できた!
- 毎週実施しているライブラリアップデートによってfirebase-bomのバージョンが上がる(33.3.0 → 33.4.0)
- ベースブランチを更新して、再度確認
- なぜか失敗するようになった。。。
なにがつらいってエラーログから得られる情報が皆無なんですよね。。
* 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を使う手順
- firebase-bomのバージョンを33.3.0に下げる
- roborazzi.gradleのコメントアウトを外す
-
git switch develop
(ベースブランチがdevelpなのでそうしています) ./gradlew recordRoborazzi
git switch feature/hoge
./gradlew compareRoborazzi
-
fd _compare --no-ignore --extension png -X mv {} .
(_compareで終わるpngファイルを抽出)- fdっていうfindの代替コマンドを使用しているので、findを使う場合は読み替えてください
どうやらapp-moduleだけが失敗するらしい
その後も時間があるときに原因を調査していました。
recordRoborazzi
ではなくapp:recordRoborazzi
やhoge: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にいるので、適当なモジュールに移動させる
株式会社 カラビナテクノロジーは「命綱や支点を素早く確実に繋ぐカラビナ。そんなカラビナのような役割をテクノロジーで実現したい」という想いのもと、福岡で設立。 主にシステム開発・アプリ開発・ Webサイト制作を行っています。採用情報→karabiner.tech/recruit/requirements/
Discussion