やらなイカ?

たぶん、iOS/Androidアプリの開発・テスト関係。

UPMパッケージのテストワークフロー事例

自作UPM (Unity Package Manager)パッケージをGitHub Actions上でテストするためのワークフローが確立できたので紹介します。

前提とするのは、リポジトリのルートがパッケージのルートディレクトリである(package.jsonがある)構成です。 Unityプロジェクトの一部をUPMパッケージとして公開している構成でも、パスを書き換えるなどすることで応用できるはずです*1

また、テストアセンブリの名前末尾が .Tests であることを前提としています*2

実現していることは次のとおりです。

  • 先行ジョブのキャンセル
  • 複数Unityバージョンでのテスト実行(互換性)
  • テスト実行用Unityプロジェクトの生成
  • テスト実行のための依存関係の解決
  • テスト実行
  • コードカバレッジの集計
  • Slack通知

以下、処理順に説明します。 記事の最後に、実際に動作しているリポジトリのURLを紹介しています。

ワークフロー解説

前提として、このワークフローは、Pull Requestへのpushおよび、masterブランチへのマージで実行されます。ただしMarkdownファイルおよび test.yml 以外のワークフローの更新ではトリガしないようにしています。

on:
  push:
    branches:
      - master
    paths-ignore:
      - '**.md'
      - '.github/'
      - '!.github/workflows/test.yml'
  pull_request:
    types: [ opened, synchronize, reopened ]  # Same as default
    paths-ignore:
      - '**.md'
      - '.github/'
      - '!.github/workflows/test.yml'

先行ジョブのキャンセル

同一ブランチでワークフロー動作中に新たなpushがあったとき、先行しているジョブをキャンセルするよう設定しています。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

複数Unityバージョンでの実行設定

matrixにテストを実行するUnityバージョンを設定します。 includeは、コードカバレッジの集計を実行するバージョン1つ(下例ではUnity 2022.2.5f1)にのみoctocovフラグを立てています。

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        unityVersion:
          - 2019.4.40f1
          - 2020.3.42f1
          - 2021.3.15f1
          - 2022.2.5f1
        include:
          - unityVersion: 2022.2.5f1
            octocov: true

以降のステップは、Unityバージョンごとに実行されます。

チェックアウト

サブモジュールがある場合はsubmodulestruerecursiveを指定します。 Git LFSにテスト実行に必要なファイルが存在する場合はlfsも設定します。

      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          submodules: false
          lfs: false

テスト実行用Unityプロジェクトの生成

テスト実行に使用する空のUnityプロジェクトをUnityProject~ディレクトリに生成します*3。 末尾に~をつけることで、Unityの管理対象にならないようにしています。

      - name: Crete project for tests
        uses: nowsprinting/create-unity-project-action@v2
        with:
          project-path: UnityProject~

空プロジェクトの生成には自作のActionを使用しています。Unityライセンス不要*4で、Unity 2018.1.0f1*5プロジェクトを生成するものです。

github.com

テスト実行のための依存関係の解決

最低限、リポジトリ直下のディレクトリをPackages/manifest.jsondependenciesに追加する必要がありますし、UniTaskなどサードパーティのパッケージに依存している場合はScoped Registriesの設定も必要です。

また、Unity Test FrameworkパッケージのアップデートおよびCode Coverageパッケージもインストールしておきます*6

manifest.jsonの更新には、openupm-cliを使用しています。

      - name: Set package name
        run: |
          echo "package_name=$(grep -o -E '"name": "(.+)"' ./package.json | cut -d ' ' -f2)" >> "$GITHUB_ENV"

      - name: Install dependencies
        run: |
          npm install -g openupm-cli
          openupm add -f com.unity.test-framework@1.3.2
          openupm add -f com.unity.testtools.codecoverage@1.2.2
          openupm add -f com.cysharp.unitask@2.3.3
          openupm add -ft "${{ env.package_name }}"@file:../../
        working-directory: ${{ env.CREATED_PROJECT_PATH }}

openupm add-fオプションは、インストールするパッケージのバージョン制限を無視してくれます。 この時点でUnityプロジェクトは2018なので、前提条件を満たさないパッケージもあるためです。 実際に使用するUnityバージョンはテスト実行時に指定し、そこでアップデートが走ります。

また-tオプションは、manifest.jsontestablesに追加し、パッケージをテスト実行対象にしてくれるものです。

テスト実行

テスト実行前に、Code Coverageパッケージのフィルタを組み立てます。 フィルタを適切に設定しないと開発対象外のコードまで含まれてしまいテスト実行に時間がかかりますし、カバレッジの数値にも影響します。 またテストコードは常にカバレッジ100%になるため、これも除くべきです。

次のように、リポジトリに含まれるasmdefファイルすべて(末尾.Testsは除く)と、Assets下を対象にしています*7

      - name: Set coverage assembly filters
        run: |
          assemblies=$(find . -name "*.asmdef" -maxdepth 3 | sed -e s/.*\\//\+/ | sed -e s/\\.asmdef// | sed -e s/^.*\\.Tests//)
          echo "assembly_filters=$(echo ${assemblies[*]} | sed -e s/\ /,/g),+<assets>,-*.Tests" >> "$GITHUB_ENV"

テスト実行にはgame-ci/unity-test-runnerを使用しています。ポイントは、

  • unityVersionにmatrixのUnityバージョンを指定
  • projectPathに生成したUnityプロジェクトのパスを設定
  • customParametersにはGitHub Actiuons上で実行したくないテストをスキップする指定

coverageOptionsの設定内容については Using Code Coverage in batchmode | Code Coverage | 1.2.3 を参照してください。

      - name: Run tests
        uses: game-ci/unity-test-runner@v2
        with:
          githubToken: ${{ secrets.GITHUB_TOKEN }}
          unityVersion: ${{ matrix.unityVersion }}
          checkName: test result (${{ matrix.unityVersion }})
          projectPath: ${{ env.CREATED_PROJECT_PATH }}
          customParameters: -testCategory "!IgnoreCI"
          coverageOptions: generateAdditionalMetrics;generateTestReferences;generateHtmlReport;generateAdditionalReports;dontClear;assemblyFilters:${{ env.assembly_filters }}
          UNITY_LICENSE: ${{ secrets[env.secret_key] }}
        id: test

コードカバレッジの集計

Code Coverageパッケージv1.2からカバレッジ情報をLCOV形式でも出力してくれるようになったため、octocovによる集計が可能になりました。 カバレッジやcode:test比の増減をPull Requestコメントやジョブサマリに貼ってくれたりするので便利です。

事前に.octocovファイルにあるカバレッジレポートのパスをgame-ci/unity-test-runnerの出力パスに書き換えてから実行しています。

      - name: Set coverage path for octocov
        run: sed -i -r 's/\.\/Logs/${{ steps.test.outputs.coveragePath }}/' .octocov.yml
        if: ${{ matrix.octocov }}

      - name: Run octocov
        uses: k1LoW/octocov-action@v0
        if: ${{ matrix.octocov }}

このステップはレポートの重複を避けるため、matrixに設定したoctocovフラグの付けられたバージョンでのみ実行しています。

Slack通知

matrixで実行した全テストの終了を待ち合わせてSlackで通知しています。 待ち合わせはneedsで、またテスト失敗も拾うためにif: always()を追加しています。

通知にはGamesight/slack-workflow-statusを使用しています*8

  notify:
    needs: test
    runs-on: ubuntu-latest
    if: always()

    steps:
      - uses: Gamesight/slack-workflow-status@v1.2.0
        with:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}

このワークフローが完走したときのジョブは次のようになります。

リポジトリ紹介

紹介したワークフローは、次のリポジトリで実際に使用しています*9。ワークフローはどれもほぼ同じものを使い回せています(依存パッケージの違い程度)。

blender-like-sceneview-hotkeys

Blender 的なテンキー操作でSceneViewの視点を操作できるエディタ拡張

github.com

紹介記事

www.nowsprinting.com

create-script-folders-with-tests

C#スクリプトを置くRuntimeとEditor、およびそれぞれのTestsフォルダとasmdefを生成してreferencesの設定まで行なうエディタ拡張*10

github.com

test-helper.monkey

uGUIを対象としたモンキーテストのリファレンス実装とヘルパーライブラリ(いまのところ)

github.com

紹介したAction・ツールまとめ

github.com

github.com

github.com

github.com

github.com

宣伝

テストの書きかたについてはこちらを参照

www.nowsprinting.com

*1:作業用Unityプロジェクトをセットでリポジトリに入れていればそのままテスト実行できるのですが、互換性テストではUnityバージョンをダウングレードできないため、プロジェクトのUnityバージョンを上げずにキープするか、作業用とは別にテスト実行時用のUnityプロジェクトを用意する必要があります。後者の場合、この記事が応用できるはずです

*2:正確にはasmdefファイル名で判断しています。本来であればファイルの中身を見るべきなのですが一致している前提でサボっています

*3:都度生成するので、リポジトリに含まないよう.gitignoreに追加してあります

*4:プロジェクトのテンプレートをコピーしているだけなので

*5:UPMサポートが入ったバージョン

*6:例では依存パッケージのバージョンを指定していますが、省略して常に最新バージョンを使用することもできます

*7:通常Assetsは空ですが、Samplesに含まれるテストを実行するためにコピーするケースを想定しています

*8:game-ci/unity-test-runnerと若干相性が悪く、skipされるテストがあると失敗扱いで通知されてしまいます。落ち着いたらどちらかを改善できればと思いつつ…

*9:Unity 2020と2021は実行を省略しています

*10:Unity 2020以降のLinux editorでテストが失敗していますが利用はできます。Unityにバグレポ済み