React Nativeのプロジェクトをgithub actions上でスクリーンショットテストを実装してみた

モバイルエンジニアインターン生の筒井です。 本記事では、私が入社して行ってきたReact Nativeプロジェクトの、GitHub Actions上でのスクリーンショットテスト実装についてお話しします。 これによりGithub Actions上でReact Nativeアプリが動き、そのスクリーンショットが保存され、確認できるようになります。

背景

アプリ開発において、自動テストを行うことでコード内のエラーなどに気づくことができます。 しかし、Natureでは自動テストでロジックの確認は行なっていますが、現状、画面のレイアウト崩れは検知することができません。 そのため、実際に実装画面を見てみないと分からないデザインの崩れや画面遷移の確認などは、手元で動かして確認しています。 手元での確認は、 ただ、この確認作業はビルド→起動→遷移の確認...と手順が多く手間な作業です。 そこで、GitHub Actions上で実際にアプリを動作させ、その際のスクリーンショットを保存して一度に画面遷移を確認を行いたいというのが今回のモチベーションになります。

なお、数あるテストフレームワークの中で今回はDetoxと呼ばれるフレームワークを利用して実装を行いました。 (本記事では、テストフレームワークの選定などの話には言及せず、実装方法について詳しくお話しします。)

github.com

Detox設定・使い方

Detoxを利用するためには、まず package.json に必要な設定を追加し、detox.config.js などで環境を定義します。 本章では、Detoxの基本的な使い方として、テスト対象の要素を操作する方法や、スクリーンショットの取得方法を紹介します。

1. Detoxのインストール

Detox をプロジェクトに追加します。 その後、必要なパッケージなどを全てインストールします。

# このページを参考にプロジェクトの設定を行う(https://wix.github.io/Detox/docs/next/introduction/environment-setup)
npm install detox-cli --global
brew tap wix/brew  
brew install applesimutils 
npm install "jest@^29" --save-dev 
npm install detox --save-dev 

2. detox.config.js の設定

Detoxを実行するために、<PROJECT_ROOT>/detox.config.jsでデバイスの設定を行います。

module.exports = {
  testRunner: "jest",
  runnerConfig: "e2e/jest.config.js",
  apps: {
    "ios.sim.debug": {

      # バイナリファイルが生成されるパスを指定
      # 本プロジェクトでは、xcodebuildコマンドのderivedDataPathオプション利用することで、バイナリファイルのパスを指定
      binaryPath: "ios/build/Build/Products/Debug-iphonesimulator/Nature.app",
      build: "xcodebuild -workspace ios/Nature.xcworkspace -scheme Nature -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
    },
  },
  devices: {
    "ios.sim.debug": {
      type: "ios.simulator",

      # 使いたいデバイス名を指定
      device: {
        type: "iPhone 16 Pro",
      },
    },
  },
  configurations: {
    "ios.sim.debug": {
      device: "ios.sim.debug",
      app: "ios.sim.debug",
    },
  },
};

ここで

  • binaryPath にアプリのビルド後の .app のパスを指定
  • build に xcodebuild のビルドコマンドを設定
  • device.type に使用する シミュレーター を指定

これにより、detox test --configuration ios.sim.debug を実行すると、指定したシミュレーターでテストが動作するようになります。

3. テスト画面の作成

Detoxでは、React Native アプリをプログラム上で自動操作 することができます。 ただし、動かしたい画面には事前にUI要素を識別するために testIDを設定する必要があります。

例: testIDの設定

  • 想定画面

  • コード内でのtestIDの割り当て

  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.text}>ログイン画面</Text>
      <TextInput
            testID="input-id" # ID入力フォームにtestIDを指定
            style={styles.input}
            placeholder="ID"
            value={id}
            onChangeText={setId}
            autoCapitalize="none"
      />
      <TextInput
            testID="input-password" # PASSWORD入力フォームにtestIDを指定
            style={styles.input}
            placeholder="PASSWORD"
            value={password}
            onChangeText={setPassword}
            secureTextEntry
      />
      <Button title="ログイン" 
          onPress={handleLogin} 
          testID={"login-button"} # ログインボタンにtestIDを指定
      />
     </SafeAreaView>
  );
}

テストの作成

testIDの割り振り後、テストを動かすためのテストコードを作成します。 割り当てた要素ごとに、タップ、スワイプ、テキスト入力などを行うことが可能です。

  it('screenshot test', async () => {
    # 初期画面をスクリーンショット
    await device.takeScreenshot('first screen');

    # input-idの要素にIDを入力
    # ID入力後の画面をスクリーンショット
    await element(by.id('input-id')).tap();
    await element(by.id('input-id')).typeText('admin');
    await device.takeScreenshot('input_id');

    # input-passwordの要素にPWを入力
    # PW入力後の画面をスクリーンショット
    await element(by.id('input-password')).tap();
    await element(by.id('input-password')).typeText('admin');
    await device.takeScreenshot('input_pass');

    # login-buttonの要素をタップ
    # 画面遷移後の画面をスクリーンショット
    await element(by.id('login-button')).tap();
    await device.takeScreenshot('After_login');
  });

4. Detoxを自分のマシンで実行

# アプリをビルド
detox build --configuration ios.sim.debug

# テストを実行
detox test --configuration ios.sim.debug

このコマンドでシミュレーターを起動し、テストを実行できます。

① 初期画面


await device.takeScreenshot('first screen');
     

② ID入力


await element(by.id('input-id')).tap();
await element(by.id('input-id')).typeText('admin');
await device.takeScreenshot('input_id');

input-idの要素をタップしてIDを入力
その後スクリーンショットを取得

③ PW入力


await element(by.id('input-password')).tap();
await element(by.id('input-password')).typeText('admin');
await device.takeScreenshot('input_pass');

input-passwordの要素をタップしてPWを入力
その後スクリーンショットを取得

④ 画面遷移後


await element(by.id('login-button')).tap();
await device.takeScreenshot('After_login');

login-buttonの要素をタップして画面遷移
その後スクリーンショットを取得

保存された画像は、<PROJECT_ROOT>/artifacts以下に保存されます。

以上で、 Detoxの使い方に関する解説は以上になります。 次の章では、github actions上での実装方法について述べていきます。

github actionsの環境構築

続いて、自動テストを動かすために必要な環境構築についてです。

GitHub Actionsでテストを動かすためには、ワークフロー (Workflow)と呼ばれるYAMLファイルを定義する必要があります。 YAMLファイルは、<PROJECT_ROOT>/.git/workflows以下に作ります。 このワークフローに従い、環境構築や行いたい自動テストを実行します。 本テストで実装したワークフローは以下のようなコードになっております。

name: Detox Test
# 任意のタイミングでテストを実行
on:
  workflow_dispatch:

jobs:
  build:
    runs-on: macOS-15
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          submodules: recursive
      - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
        with:
          node-version: 18
      - name: Initialize and update submodules
        run: |
          git submodule sync
          git submodule update --init --recursive
      - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
        with:
          xcode-version: '16.0'
      - uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0
        with:
          ruby-version: '3.3'
          bundler-cache: true

      - name: Config
        run: |
          gem install bundler
          brew install cocoapods
          npm install --legacy-peer-deps
          cd ios
          rm -rf .xcode.env.local
          rm -rf Podfile.lock
          bundle exec pod install --repo-update

      # バイナリファイル生成
      - name: build
        run: xcodebuild -workspace ios/Nature.xcworkspace -configuration Debug -scheme Nature -destination "platform=iOS Simulator,name=iPhone 16,OS=18.0" -derivedDataPath ios/build

      - name: Install macOS dependencies
        run: |
          npm install detox-cli --global 
          brew tap wix/brew
          brew install applesimutils
          brew install tmux

      - name: Test
        run: |
          # devサーバ起動
          tmux new-session -d -s metro 'npm start --port 8081 --reset-cache'

          # シミュレータ起動
          xcrun simctl boot "iPhone 16"
          xcrun simctl install booted /Users/runner/work/nature-app/nature-app/ios/build/Build/Products/Debug-iphonesimulator/Nature.app
          xcrun simctl launch booted global.nature.Remo

          # スクリーンショットテスト実行
          detox test --configuration ios.sim.debug --take-screenshots all

      # 生成した画像をアップロード
      - name: Upload artifacts
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: detox-artifacts
          path: ./artifacts

ワークフローは基本的に下記の手順で動くようになっております。

  1. 基本設定(OSのバージョン指定など)
  2. xcodeビルドによるバイナリファイル生成
  3. Detoxのインストール
  4. devサーバを起動させ、Detoxのテストを実行
  5. 保存した画像をアップロード

スクリーンショット画像はgithub actionsでテストを動かしたページの下記からダウンロードできるようになっています。

まとめ

今回は、React NativeプロジェクトのGitHub Actions上でのスクリーンショットテスト実装を行いました。 今回のテスト実装を通じて、Github Actionsの実装の難しいところや利便性について体験することができました。 今後の展望としては、スクリーンショットした画像を前回と比較して出力できるようにしたいと思います。 この記事を参考に、GIthub Actions上でのスクリーンショットテストをぜひ試してみて下さい!

インターンの感想&すゝめ

Natureでは、約1年間インターン生として働かせていただき、とても貴重な経験を積むことができました。 これまでは短期的な開発経験しかなかったのですが、Natureのインターンでは1年間プロダクト開発に携わることができ、開発の進め方やチームでの連携について多くを学ぶことができました。 また、「これをやってみたい」と自分から発信したことに対して、積極的に任せてもらえる風土があり、主体的に学ぶ姿勢を後押ししてくれる環境でした。 その一方で、エンジニアとしての足りない部分を的確にフィードバックしてもらえる機会も多く、自分の課題としっかり向き合うことができました。 インターンは基本的にリモートでの勤務でしたが、わからないことがあればすぐにハドルなどで相談できる体制が整っており、距離を感じることなく安心して働けました。

私の感想がこれからNatureでインターンを考えている方々の参考になれば幸いです!