📸

Roborazziでテスト用のコードを書かずにスクリーンショットテストを追加する

2024/10/01に公開

はじめに

先日開催されたDroidKaigi 2024の「仕組みから理解する!Composeプレビューを様々なバリエーションでスクリーンショットテストしよう」のセッションにて、Roborazziで@Previewのプレビューを使用してスクリーンショットテストを行うことができる機能の説明がありました。
元々Roborazziを使ってスクリーンショットテスト(VRT)を行っていましたが、テスト用のコードを記述するのに煩わしさを感じていたところでもあったため、これはすごい!と思い、試してみました。
サンプルコードはこちらになります。

使ってみた感想

そこまで大きな手間なく@Previewで作ったものをスクリーンショットテスト(VRT)に使いまわせるようになるため、かなり便利だと感じました。
これに加え、前述のセッションでも言及されていたデバイス情報のパラメータを使用すると、かなりコスパ良くスクリーンショットテストを行うことができるのではないかと思います。

実際に触ってみて、

  • 自分の環境ではroborazzi-compose-preview-scanner-supportが必須であったが、エラーメッセージ等に出力されなかった
  • スクリーンショットの出力先を変えるのはちょっと手間だった

といった点は留意しておくと良さそうです。

使ってみる

以下は実際に試してみた手順を紹介していきます。

テストする画面を作る

まずはテスト対象とする画面を用意します。
今回はごく簡単にTopAppBar、Text、TextButtonと、カスタムしたスナックバーを表示するのみの画面を用意しました。

テスト対象の画面

Roborazziを導入する

続いてRoborazziおよび必要なライブラリであるRobolectricを導入します。

libs.versions.toml
[versions]
robolectric = "4.13"
roborazzi = "1.26.0"

[libraries]
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
roborazzi-core = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }
roborazzi-rule = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" }

[plugins]
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }

[bundles]
roborazzi = [ "roborazzi-core", "roborazzi-compose", "roborazzi-rule" ]
build.gradle.kts
plugins {
    alias(libs.plugins.roborazzi)
}

android {
    testOptions {
        unitTests {
            isIncludeAndroidResources = true
        }
    }
}

dependencies {
    testImplementation(libs.androidx.espresso.core)
    testImplementation(libs.robolectric)
    testImplementation(libs.bundles.roborazzi)
}
build.gradle.kts(project root)
plugins {
    alias(libs.plugins.roborazzi) version libs.versions.roborazzi apply false
}

ここまでで、以下のようなテストコードを追加することによりスクリーンショットテストを行うことができるようになりました。

MainScreenScreenShotTest
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class MainScreenScreenShotTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun captureMainScreen() {
        ActivityScenario.launch(MainActivity::class.java)

        captureRoboImage(
            filePath = "screenshots/${this.javaClass.name}.png",
        ) {
            MainScreen()
        }
    }
}

作成したスクリーンショット

プレビューからスクリーンショットを自動生成する

ここからが本題となります。

まず、Previewからスクリーンショットを生成するのにComposablePreviewScannerを使用するため、そちらのHow to set upを参考に、ComposablePreviewScannerを導入します。

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
libs.versions.toml
[versions]
composablePreviewScanner = "0.3.0"

[libraries]
composablepreviewscanner = { group = "com.github.sergio-sastre.ComposablePreviewScanner", name = "android", version.ref = "composablePreviewScanner" }
build.gradle.kts
dependencies {
    testImplementation(libs.composablepreviewscanner)
}

これでComposablePreviewScannerの準備は完了です。

続いてRoborazziの設定を行います。
READMEにあるようにgenerateComposePreviewRobolectricTestsのenableをtrueに設定します。

build.gradle.kts
roborazzi {
    @OptIn(ExperimentalRoborazziApi::class)
    generateComposePreviewRobolectricTests {
        enable = true
    }
}

すると、以下のエラーが発生しました。

A problem occurred configuring project ':app'.
> java.lang.IllegalStateException: Please set roborazzi.generateComposePreviewRobolectricTests.packages in the generatePreviewTests extension or set roborazzi.generateComposePreviewRobolectricTests.enable = false.See https://github.com/sergio-sastre/ComposablePreviewScanner?tab=readme-ov-file#how-to-use for more information.

エラー内容に従ってテスト対象とするパッケージを追記します。

build.gradle.kts
roborazzi {
    @OptIn(ExperimentalRoborazziApi::class)
    generateComposePreviewRobolectricTests {
        enable = true
        packages = listOf("com.kktaro.roborazzipreviewsample")
    }
}

その後Syncすると以下のメッセージが表示されました。

Roborazzi: Please set 'robolectric.pixelCopyRenderMode = hardware' (Robolectric 4.12.2+) in the 'testOptions' block in the 'build.gradle' file. This is advisable to avoid issues with the fidelity of the images.

こちらも指示に従って追加します。

build.gradle.kts
testOptions {
    unitTests {
        isIncludeAndroidResources = true
        all {
            it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware"
        }
    }
}

これでさらにSyncするとメッセージが出なくなりました。

のでいざ./gradlew recordRoborazziDebugを実行すると...

Unresolved reference 'getComposePreviewTester'.

上記エラーでこけてしまいました。

どうやら自動生成されるファイルに含まれるComposePreviewTesterのインターフェースがio.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-supportパッケージに定義されているようなので、こちらを依存関係に追加します。

libs.versions.toml
[libraries]
roborazzi-composablepreviewscanner = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose-preview-scanner-support", version.ref = "roborazzi" }

[bundles]
roborazzi = [ "roborazzi-core", "roborazzi-compose", "roborazzi-rule", "roborazzi-composablepreviewscanner" ]

これで@Previewにより設定したプレビューのスクリーンショットが撮れるようになりました🎉

が、

  • privateのプレビューのスクリーンショットが撮れない
  • スクリーンショットの出力先がbuild/配下にある

の問題があるため、これらの設定を行います。

privateプレビューのスクリーンショットを追加する

こちらはREADMEにあるように設定を有効化するのみでOKです。

build.gradle.kts
roborazzi {
    @OptIn(ExperimentalRoborazziApi::class)
    generateComposePreviewRobolectricTests {
        enable = true
        packages = listOf("com.kktaro.roborazzipreviewsample")
        includePrivatePreviews = true
    }
}

これによりprivateで定義しているプレビューのスクリーンショットが出力されるようになります。
private

スクリーンショットの出力先を変更する

./gradlew verifyRoborazziDebugによりUIの変更を検知できるようにするため、スクリーンショットもGit管理に含めるように変更します。
デフォルトではbuild/配下に出力されるため、app/screenshotに出力されるように変更します。
出力先を直接修正するプロパティは生えていないので、ComposePreviewTesterを自前で実装する必要があります。
出力先を決定しているのはRoborazziが提供しているAndroidComposePreviewTestertestメソッドのcaptureRoboImageの部分です。
https://github.com/takahirom/roborazzi/blob/main/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt#L100-L113

こちらを参考に、自前のComposePreviewTesterを実装し、任意の出力先に変更します。

MyComposePreviewTester
@OptIn(ExperimentalRoborazziApi::class)
class MyComposePreviewTester : ComposePreviewTester<AndroidPreviewInfo> {
    override fun previews(): List<ComposablePreview<AndroidPreviewInfo>> {
        val options = options()
        return AndroidComposablePreviewScanner()
            .scanPackageTrees(*options.scanOptions.packages.toTypedArray())
            .let {
                if (options.scanOptions.includePrivatePreviews) {
                    it.includePrivatePreviews()
                } else {
                    it
                }
            }
            .getPreviews()
    }

    override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
        val name = AndroidPreviewScreenshotIdBuilder(preview)
            .ignoreClassName()
            .build()
        val filePath = "screenshot/$name.png"
        preview.captureRoboImage(filePath)
    }
}

更に作成したファイルのパスをRoborazziに渡すことで、自前で用意したものが使われるようになります。

build.gradle.kts
roborazzi {
    @OptIn(ExperimentalRoborazziApi::class)
    generateComposePreviewRobolectricTests {
        enable = true
        packages = listOf("com.kktaro.roborazzipreviewsample")
        includePrivatePreviews = true
        testerQualifiedClassName = "com.kktaro.roborazzipreviewsample.MyComposePreviewTester"
    }
}

これによりapp/screenshots/にスクリーンショットが出力されるようになりました🎉

https://github.com/kktaro/RoborazziPreviewSample/tree/main/app/screenshots

株式会社ガラパゴス(有志)

Discussion