スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

スタディサプリ小学・中学講座でRoborazziを導入しました

こんにちは、Androidエンジニアの@morux2です。本記事ではスクリーンショットの撮影にRoborazziを導入した経緯をご紹介できればと思います。

はじめに

スタディサプリ小学・中学講座では、UnitTestに加えてVisual Regression Test (以下 VRT)を行っています。VRTは画像比較によるUIの回帰テストです。変更前後のコードそれぞれに対する画面のスクリーンショットを比較し、意図しない差分を検知することができます。*1

VRTは、画面の用意・撮影・比較の3ステップに分けることができます。これまでは以下の実装方法になっていました。*2*3

  1. 画面の用意
  2. 撮影
  3. 比較

今回は3つのステップのうち、撮影の部分をRoborazziに移行しました。RoborazziAndroid端末を使わずに、JVM上でスクリーンショットを撮影することができるライブラリです。Robolectricのグラフィック機能がベースとなっており、AndroidTestではなくUnitTestとして実行することができます。

きっかけ

2023年9月、スタディサプリ中学講座に小学コンテンツが追加され、小学・中学講座に生まれ変わりました。*4 小学講座はタブレット専用のUIになっているので、これまでのスマートフォンでのVRTに加えてタブレットでの実行が必要不可欠になりました。

そこで、近年主流となっているJVM上でスクリーンショットを撮影できるライブラリの導入を検討しました。コストや実行時間の削減、複数端末での撮影を期待したためです。

RoborazziとPaparazziの比較

今回JVM上でスクリーンショットを撮影できるライブラリとして、PaparazziとRoborazziを比較検討しました。観点は以下になります。

  • 書きやすさ
  • 複数端末での撮影
  • スクロールした画面の撮影
  • Showkaseの流用

スマートフォンタブレット両者での撮影はもちろん、複数の画面サイズでの品質担保も必要でした。特に小学講座は8インチ以上のタブレットを動作対象としているため、10~11インチの推奨サイズに加えて小さい端末での見た目も確認する必要があります。また、中学講座ではほとんどの画面で縦スクロールするため、スクロールした画面の撮影が必要でした。

書きやすさ

Paparazzi, Roborazziともにメソッドを呼び出すだけで手軽にスクリーンショットが撮影出来ます。どちらも現状はJUnit4で実行することになるので、JUnit5に移行をしている場合はVintageライブラリを使って実行することになります。*5

PaparazziとRoborazziのサンプルコード Paparazzi

import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5
import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

class MySnapshotTest {
   @get:Rule
   val paparazzi = Paparazzi(deviceConfig = PIXEL_5)

   @Test
   fun captureMyComposableScreen() {
       paparazzi.snapshot {
           MyComposableScreen()
       }
   }
}

Roborazzi

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.Pixel5)
class MySnapshotTest {
   @get:Rule
   val composeTestRule = createComposeRule()

   @Test
   fun captureMyComposableScreen() {
       composeTestRule.setContent {
           MyComposableScreen()
       }
       composeTestRule
           .onRoot()
           .captureRoboImage()
   }
}

複数端末での撮影

両者ともに複数端末での撮影は可能です。

PaparazziとRoborazziのサンプルコード Paparazzi

import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(TestParameterInjector::class)
class MySnapshotTest {
   @get:Rule
   val paparazzi = Paparazzi()

   enum class Device(public val deviceConfig: DeviceConfig) {
       PIXEL_5(DeviceConfig.PIXEL_5),
       PIXEL_C(DeviceConfig.PIXEL_C)
   }

   @Test
   fun captureMyComposableScreen(
       @TestParameter device: Device,
   ) {
       paparazzi.unsafeUpdateConfig(deviceConfig = device.deviceConfig)
       paparazzi.snapshot(name = device.name) {
           MyComposableScreen()
       }
   }
}

Roborazzi

@Configアノテーションを用いる方法*6

import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.captureRoboImage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class MySnapshotTest {
   @get:Rule
   val composeTestRule = createComposeRule()

   @Test
   @Config(qualifiers = RobolectricDeviceQualifiers.Pixel5)
   fun captureMyComposableScreenPixel5()= captureMyComposableScreen()

   @Test
   @Config(qualifiers = RobolectricDeviceQualifiers.PixelC)
   fun captureMyComposableScreenPixelC()= captureMyComposableScreen()


   private fun captureMyComposableScreen() {
       composeTestRule.setContent {
           MyComposableScreen()
       }
       composeTestRule
           .onRoot()
           .captureRoboImage()
   }
}

Parametarizedテストを用いる方法

import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.GraphicsMode

@RunWith(ParameterizedRobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class MySnapshotTest(
   private val device: Device,
) {
   @get:Rule
   val composeTestRule = createComposeRule()

   enum class Device(public val robolectricDeviceQualifiers: String) {
       PIXEL_5(RobolectricDeviceQualifiers.Pixel5),
       PIXEL_C(RobolectricDeviceQualifiers.PixelC)
   }

   @Test
   fun captureMyComposableScreen() {
       RuntimeEnvironment.setQualifiers(device.robolectricDeviceQualifiers)
       composeTestRule.setContent {
           MyComposableScreen()
       }
       composeTestRule
           .onRoot()
           .captureRoboImage()
   }

   companion object {
       @ParameterizedRobolectricTestRunner.Parameters
       @JvmStatic
       fun data(): Array<Device> {
           return Device.values()
       }
   }
}

スクロールした画面の撮影

スマホで閲覧する中学講座アプリはほとんどの画面で縦スクロールするため、スクロール後UIの品質担保を重視しました。RoborazziはRobolectricベースなので、composeTestRuleをUnitTestで呼び出してスクロールを実行することが可能です。一方Paparazziではスクロールを実行できません。代わりに縦長画像を撮影できる機能が検討されていますが、1年近く進展がありません。

Roborazziのサンプルコード

import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performScrollToNode
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.Pixel5)
public class MySnapshotTest {
   @get:Rule
   val composeTestRule = createComposeRule()

   @Test
   fun captureMyComposableScreen() {
       composeTestRule.setContent {
           MyComposableScreen()
       }
       composeTestRule
           .onNodeWithTag("lazyColumn")
           .performScrollToNode(hasText("Hello"))
           .captureRoboImage()
   }
}

Showkaseの流用

Preview流用によるテストコードの削減も欠かせませんでした。Paparazzi・RoborazziともにShowkaseを流用し、@PreviewのついたComposableのキャプチャを一括で撮影することが可能です。詳細な説明は割愛しますので、DroidKaigi conference-app-2022および DroidKaigi conference-app-2023をご確認ください。

Roborazziの採用理由

書きやすさ・複数端末での撮影・Showkaseの流用は両者差がありませんでしたが、スクロールの実行はRoborazziのみ可能でした。RoborazziのREADMEでも同様の言及があり、RobolectricベースであるためにHiltでFakeをInjectできること、UIに対してスクロールやタップアクションを実施できることが優位性として述べられています。

Google I/O 2023 では、公式からHost-side Screenshot Testingの発表もありましたが、AndroidStudioのPreview機能がベースとなっているため、こちらもスクロールの実行は厳しいのではないかと推測されています。*7

以上を踏まえて、我々はRoborazziの採用を決定しました。検討の段階でnowinandroidにRoborazziが導入されたことも大きな決め手となりました。

現状の運用

中学講座は今まで通り実機でスクリーンショットを撮影し、小学講座のみRoborazziを利用しています。今後は中学もRoborazziに移行し、小学については複数端末での実行を検討していきます。

  1. 画面の用意
    • @PreviewのついたComposable
      • Showkase + ComposeTestRule
    • スクロールが必要な画面
      • ComposeTestRule(performScrollToNode)
  2. 撮影
    • 小学講座 (new!)
      • Roborazzi
    • 中学講座
      • ComposeTestRule(captureToImage) + Firebase Test Lab
  3. 比較
    • reg-suit

VRTの結果画面

さいごに

今回はスタディサプリ小学・中学講座でRoborazziを導入した経緯を紹介しました。Roborazziの移行が進みましたら、Tips等も共有できればと思います。まだまだ試行錯誤の段階なので、常により良い方法やアイデアを歓迎しています。

スタディサプリでは、一緒に最高のプロダクトを作っていってくれる仲間を募集しています! 少しでもご興味がある方はこちらのページからご連絡ください! brand.studysapuri.jp