14
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

N予備校 Android チームでテックリードをしている鎌田です。
N予備校 Android チームでは、スクリーンショットテストを導入しました。
本記事では、どのようにスクリーンショットテストを導入したか、背景と手順をまとめます。
スクリーンショットテストの概要については、他のスクリーンショットテストに関する記事でも説明されているため、参考 URL を参照してください。

背景

PR を作成する際、UI に変更がある場合は修正前後の画面キャプチャを貼り付けるようにしていました。
ですが、都度画面キャプチャを自前で用意しなければいけないのが面倒なのと、ぱっと見ではわかりづらいデザイン変更の場合だと画面キャプチャからでもわかりづらいことがありました。
そこで、DroidKaigi 2023 アプリのように PR 上で差分のある UI に対してのみスクリーンショットの差分を提示することで、UI に変更がないことを証明でき、かつ画面キャプチャが不要になり手間が減るのではと考えました。
参考:Adding tests to BookmarkScreenTest by hiesiea · Pull Request #932 · DroidKaigi/conference-app-2023

前提

Jetpack Compose を使用していること。
Jetpack Compose を使用していない UI については今回の方法ではできませんのでご承知おきください。
また、本アプリではマルチモジュールを採用しております。
モジュール構成図は以下の通りです。

module_architecture.png

環境

RoborazziShowkaseGitHub Actions を使って構築しました。
Roborazzi を採用した理由としては、類似ライブラリである Paparazzi と比較して他社での採用事例が多く、スクロールできるなどの独自機能もあるためです。
Showkase を採用した理由としては、Preview 関数で生成した Composable をスクリーンショットテストにそのまま使用でき、コード削減できると考えたためです。
GitHub Actions を採用した理由としては、Roborazzi を実行するのに GitHub Actions の方がサンプルコードもあり実装しやすそうだったためです。
また、極力 DroidKaigi 2023 アプリのような形の実行形式を目指していたためです。

実装

ビルド周りの設定

まず、ルートの build.gradle.kts に対して以下の設定を追加します。

build.gradle.kts
plugins {
    ...
    id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false
    id("io.github.takahirom.roborazzi") version "1.10.1" apply false
}

次に、app モジュールの build.gradle.kts に対して以下の設定を追加します。
本アプリでは、上述の通り Showkase を利用してスクリーンショットテストを実装したかったため、app モジュールにのみスクリーンショットテストを作成する方針で進めました。

app/build.gradle.kts
plugins {
    ...
    id("io.github.takahirom.roborazzi")
}
...
dependencies {
    ...
    testImplementation("io.github.takahirom.roborazzi:roborazzi:1.10.1")
    testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.10.1")
}

次に、Composable を実装している各モジュール(モジュール構成図で言うところの UI モジュール)の build.gradle.kts に対して以下の設定を追加します。

build.gradle.kts
plugins {
    ...
    id("com.google.devtools.ksp")
}
...
dependencies {
    ...
    debugImplementation("com.airbnb.android:showkase:1.0.2")
    implementation("com.airbnb.android:showkase-annotation:1.0.2")
    kspDebug("com.airbnb.android:showkase-processor:1.0.2")
}

// private な Preview 関数は無視するようにし、対象を絞れるようにする
ksp {
    arg("skipPrivatePreviews", "true")
}

本アプリでは、すぐに見た目で確認できるよう、基本的に実装した Composable に対して必ず Preview 関数を作成しています。
ですが、全ての Preview 関数を対象としようとすると OutOfMemoryError が発生してテストに失敗してしまうので、対象を絞る必要がありました。
そのため、private な Preview 関数は無視するようにし、対象を絞れるようにしました。

Sample.kt
// private が付いていると Showkase 側で無視するようになるのでスクリーンショットテストの対象にならない
@Preview
@Composable
private fun SamplePreview() {
    Sample(
        ...
    )
}
 
// private を外すことで Showkase が認識できるようになるのでスクリーンショットテストの対象になる
@Preview
@Composable
fun SampleForScreenshotTestPreview() {
    Sample(
        ...
    )
}

スクリーンショットテストのコードを追加

まず、Preview 関数で生成した Composable を参照できるよう、ShowkaseRootModule インタフェースの実装を app モジュールに定義します。

ShowkaseRootModule.kt
@ShowkaseRoot
class ShowkaseRootModule : ShowkaseRootModule

次に、スクリーンショットテストを行う PreviewTest を実装します。

PreviewTest.kt
@RunWith(ParameterizedRobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(
    qualifiers = RobolectricDeviceQualifiers.Pixel7Pro,
)
class PreviewTest(
    private val showkaseBrowserComponent: ShowkaseBrowserComponent,
) {
    @Test
    fun previewScreenshot() {
        val filePath =
            DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH + "/" + showkaseBrowserComponent.componentKey + ".png"
        captureRoboImage(
            filePath,
        ) {
            showkaseBrowserComponent.component()
        }
    }

    companion object {

        @ParameterizedRobolectricTestRunner.Parameters
        @JvmStatic
        fun components(): Iterable<Array<Any?>> {
            return Showkase.getMetadata().componentList.map { showkaseBrowserComponent ->
                arrayOf(showkaseBrowserComponent)
            }
        }
    }
}

できる限り画面サイズの大きいデバイスで確認したいと思い、@Config アノテーションでデバイスは Pixel7Pro を指定しています。
Showkase.getMetadata().componentList に Showkase で抽出した Preview 関数群の情報が格納されています。
それを previewScreenshot の中で showkaseBrowserComponent として受け取り、使用しています。

次に、Android Studio 上で PreviewTest を実行する場合、gradle.properties に以下の設定を追加します。

gradle.properties
roborazzi.test.record=true

GitHub のワークフローを追加

以下のサンプルコードをほぼ流用した形で実装しました。

ただし、上記のサンプルコードはデフォルトブランチへ向けた PR のみを想定しており、それ以外だとうまく動作しないので注意してください。
また、GitHub Actions のリソースは有限であり、プランによっては他リポジトリとリソースを共有しているケースもあります。
その場合、スクリーンショットテストでリソースを食い潰さないよう、他チームとの調整が必要です。

実装結果

実行結果は以下の通りです。
例えばパディングを調整したときに、以下のように赤く差分が表示されるようになりました。

result.png

また、Compose 関連のライブラリがアップデートされたときなど、直接 UI の変更を行なっていない場合にも変更があったとわかるようになりました。

まとめ

本記事では、N予備校 Android チームでスクリーンショットテストを導入したので、背景と手順をまとめました。
スクリーンショットテストを導入したことで、PR 上で差分のある UI に対してのみスクリーンショットの差分が提示されるようになり、UI に変更があったかどうかがわかるようになりました。
特に、Compose 関連のライブラリがアップデートされたときなど、直接 UI の変更を行なっていない場合にも変更があったとわかるようになったのが大きかったです。
まだ一部の画面しか対象にしていないのと、画面キャプチャを用意する・しないについての基準が決まっていないなどあるため、そこは今後検討していきたいです。

参考 URL

14
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?