やらなイカ?

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

JetBrains Rider/ReSharperのNamespace does not correspond to file locationインスペクションをUnityプロジェクトで期待どおり診断させる

JetBrains Rider/ReSharperの提供するコードインスペクションに "Namespace does not correspond to file location" があります。 名前空間ディレクトリ階層と一致していることを検査するものですが、Unityプロジェクトの場合デフォルトでは期待どおりの結果が得られません。

例えば次のようなディレクトリ階層のとき、Foo.csの名前空間Fooに、Bar.csはFoo.Barにしたいはずです。

Assets
└── Foo
   ├── Scripts
   │   ├── Editor
   │   │   └── Foo.Editor.asmdef
   │   └── Runtime
   │       ├── Bar
   │       │   └── Bar.cs
   │       ├── Foo.cs
   │       └── Foo.asmdef
   └── Tests
       ├── Editor
       │   └── Foo.Editor.Tests.asmdef
       └── Runtime
           └── Foo.Tests.asmdef

しかし、デフォルトでは次のように診断されます。

なお、Foo.asmdefのRoot Namespaceプロパティ*1に仮にRootを設定すると、要求される名前空間Root.Foo.Scripts.Runtimeになってしまいます。

回避策(診断の無効化)

回避策として、Namespace does not correspond to file locationインスペクションを無効化する手段もあります。 RiderのPreferencesを開き、Editor | Inspection Settings | Inspection Severityで重大度を下げるか、.editorconfigファイルに次のように設定します。

resharper_check_namespace_highlighting = none

しかしこれでは、本来摘出してほしいケースでも無視されてしまいます。

期待どおり診断させる

期待通りの診断をさせるには、名前空間の組み立てルールをRiderに指示する必要があります。 具体的には、名前空間に含めたくないディレクトFoo, Runtime, Testsを無視するよう設定します*2

Riderで無視させたいフォルダを右クリックしてコンテキストメニューを開き、Properties...をクリックしてFolder Propertiesダイアログを開き、Namespace providerのチェックを外します。

これを無効化したいフォルダすべてに行ないます。 この設定は、プロジェクトのルートディレクトリにアセンブリ名.csproj.DotSettingsというファイル名で保存されますので、忘れずにVCSにコミットします。

UPMパッケージの場合

UPMパッケージの場合、次のようなディレクトリ階層が推奨されています*3

Packages
└── your.package.name
   ├── Editor
   │   └── Foo.Editor.asmdef
   ├── Runtime
   │   └── Foo.asmdef
   └── Tests
       ├── Editor
       │   └── Foo.Editor.Tests.asmdef
       └── Runtime
           └── Foo.Tests.asmdef

診断される名前空間からyour.package.nameは除かれますので、Assets下と同じようにFoo, Runtime, Testsを無視するよう設定するだけでは、Runtime下とTests下は名前空間なし、各Editor下はEditorのみとなってしまいます。

そこで、各asmdefのRoot NamespaceFooを設定することで期待どおりの名前空間を得られます。

Create Script Folders and Assemblies with Testsエディタ拡張

Create Script Folders and Assemblies with Testsは、上記のようなRuntime, TestsおよびそれぞれのEditorフォルダを生成、asmdefのAssembly Definition Referencesを適切に設定してくれるエディタ拡張です。

github.com

こちらのv1.2で、本記事にあるNamespace provider設定も行うようにしました。

openupm-cliでインストールできます。

openupm add com.nowsprinting.create-script-folders-with-tests

インストール後、フォルダを作りたいところでコンテキストメニューを開き、Create | C# Script Folders and Assemblies with Testsを選択、フォルダ名を入力すると、フォルダ構成とasmdefそしてDotSettingsファイルが生成されます。

あまり頻繁に使うものでもありませんが、便利ですのでぜひご利用ください。

蛇足

デフォルトで名前空間の解釈をしてほしい、という要望は投げたのですが*4、Unity側のAPIの問題で実現できないそうです…

参考

www.jetbrains.com

www.jetbrains.com

www.jetbrains.com

*1:Unity 2020.2で追加されました

*2:テストの名前空間にTestsを付けるかは好みの問題ですが、私は付けない派

*3:https://docs.unity3d.com/Manual/cus-layout.html

*4:https://youtrack.jetbrains.com/issue/RIDER-87752/Interpretation-of-RootNamespace-is-not-as-expected

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~ディレクトリに生成します。 末尾に~をつけることで、Unityの管理対象にならないようにしています。

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

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

github.com

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

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

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

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下を対象にしています*6

      - 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.2 を参照してください。

      - 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を使用しています*7

  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 }}

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

リポジトリ紹介

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

blender-like-sceneview-hotkeys

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

github.com

紹介記事

www.nowsprinting.com

create-script-folders-with-tests

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

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:プロジェクトのテンプレートをコピーしているだけなので

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

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

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

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

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

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

Unity Test Frameworkの非同期テストで できること/ できないこと

Unity公式のユニットテストレームワークであるUnity Test Frameworkパッケージのバージョン1.3では、非同期テスト(以下Asyncテスト)がサポートされました。

改めて、できること/ できないことを確認してまとめました。

できること

非同期(async)メソッドの呼び出し

Test属性のテストにasyncキーワードを付与することで、テストメソッド内でasyncメソッドを呼び、その完了を待つことができます。

また、SetUpおよびTearDown属性も非同期メソッドに付与できます。 ただしUnitySetUp属性とは異なり、SetUp属性によるセットアップメソッドはドメインリロード*1の後に再実行されません。

サンプルコードなどは次の記事を参照してください。

www.nowsprinting.com

パラメタライズドテスト

次の属性はUnityTest属性と組み合わせて使用できませんでしたが、Asyncテストでは使用できます。

  • TestCase属性
  • TestCaseSource属性
  • Pairwise属性
  • Sequential属性

これら属性の使いかたについては『Unity Test Framework完全攻略ガイド』の第8章「パラメタライズドテスト」を参照してください。

www.nowsprinting.com

できないこと

以下は、Unity Test Framework v1.3.2時点での制限事項です。いずれもバグレポート済みで、修正されたら本記事にも反映する予定です。

[2/24追記] Issue Trackerに登録されています(UUM-25085)。 すでにお困りの方も、今はそうでないが将来お困りになる方も、ぜひvoteをお願いします!

併用するとテストが終了しない属性

次の属性を付けたAsyncテストメソッドは終了しなくなります。

  • MaxTime属性
  • Repeat属性
  • Retry属性

Throws制約による例外の検証

次のようにThrows制約を使用した例外の検証にAsyncメソッドを指定すると、テストが終了しなくなります。

[Test]
public async Task 非同期メソッドの例外捕捉に制約モデルは使用できない()
{
    Assert.That(
        async () => await ThrowNewExceptionInMethod(),
        Throws.TypeOf<ArgumentException>());
}

回避策として、クラシックモデルAPIThrowsAsyncは使用できます*2

[Test]
public async Task 非同期メソッドの例外捕捉をThrowsAsyncで行なう例()
{
    Assert.ThrowsAsync<ArgumentException>(
        async () => await ThrowNewExceptionInMethod());
}

もしくはtry〜catchで書く方法もあります。

[Test]
public async Task 非同期メソッドの例外捕捉をTryCatchで行なう例()
{
    try
    {
        await ThrowNewExceptionInMethod();
        Assert.Fail("例外が出ることを期待しているのでテスト失敗とする");
    }
    catch (ArgumentException expectedException)
    {
        Assert.That(expectedException.Message, Is.EqualTo("message!"));
    }
}

タイムアウトが機能しない

デフォルトではテストは3分でタイムアウトし失敗扱いとなりますが、Asyncテストではこれが機能しません。 Timeout属性で時間を指定しても同様です。

WebGLプレイヤーでTask.Delayが終了しない

Play ModeテストをWebGLプレイヤーで実行するとき、await Task.Delay()を引数1以上で呼び出しているとそのテストは終了しなくなります。 async SetUp/TearDownでも同様です。

await UniTask.Delay()は動作しますので、本事象はUniTaskを使用することで回避できます。

Edit ModeテストではTask.Delayが無効

これはそもそもEdit ModeテストがPlayerLoopで動いていないためで、yield returnステートメントにWaitForSecondsなどが指定できないのと同理由と考えられます。

*1:Edit Modeテストでyield return new RecompileScripts()などを実行したときに発生します。詳しくは『Unity Test Framework完全攻略ガイド』の3.3「Edit Modeテスト固有のyield instructions」を参照してください

*2:Messageの検証も行ないたいとき、別Assertで書かないといけないのがよくないところ

UnityTestがWebGL Playerで実行できるようになっていた

Unityの標準テストフレームワークであるUnity Test Frameworkパッケージでは、非同期のテストをコルーチン様式で書くことのできる UnityTest 属性が提供されています。

この UnityTest 属性にはいくつか制限事項があり、WebGL Playerで実行できないことは公式ドキュメントにも以前から書かれていました。

具体的な振る舞いは以前記事に書きました。

www.nowsprinting.com

しかしいつの間にか公式ドキュメントから記述が消えており*1、確認してみたところ、(Unity Test Frameworkではなく)Unityエディター側の修正によって解消されていました。

該当するバグチケットはこちらです*2

issuetracker.unity3d.com

Fixed in 2020.3.42f1, 2021.3.8f1, 2022.1.12f1, 2022.2.0b3, 2023.1.0a4 とあり、実際にUnity 2021.3.8f1で動作することが確認できました*3。 また UnitySetUp および UnityTearDown 属性も同様に動作しました。

なお、Unity Test Frameworkはv1.3.2を使用しています。

また、テスト実行結果がUnityエディターのTest Runnerウィンドウに反映されない(プログレスバーが止まったままとなりキャンセルするしかない)問題も解消していました。

なお、拙書『Unity Test Framework完全攻略ガイド 第2版』ですが、昨年末に増刷したときにも本件に気づいておらず、「5.4 UnityTest属性の制限」にはWebGL Playerで動作しないと書いてあります。 今後の改版時に対応予定です。

www.nowsprinting.com

サンプルコードのほうは対応(WebGL Playerでの実行制限を削除)していますが、masterブランチはUnity 2019 LTSを維持しています。 WebGL Playerでの実行は前述のUnityバージョンにアップグレードしてお試しください。

github.com

非同期(async)テストはWebGL Playerで動作しない

蛇足です。 Unity Test Framework v1.3でサポートされた非同期(async)テストは引き続きWebGL Playerで動作しません*4。 当面はこれまでの UnityTest 属性のテスト同様、UnityPlatform 属性でWebGL Playerでの実行を抑止するしかなさそうです。

www.nowsprinting.com

*1:恐らくv1.3もしくはそれ以降のタイミング。消すのではなく情報残してほしいところ…

*2:Known limitationsに書かれていることでもバグレポートしたら対応される(ことがある)、というのは目から鱗

*3:ついでながら、1つ前のUnity 2021.3.7f1では動作しないことも確認しています

*4:ドキュメントにも書かれていない。ほかにも動かないものがあるのでまとめてバグレポートを出す予定です

#C101 ありがとうございました

コミックマーケット101、無事終わりました。 ご購入いただいた方々、また、スペースにお立ち寄りいただいた方々、ありがとうございました。

『Unity Test Framework完全攻略ガイド 第2版』は、BOOTHおよび技術書典オンラインマーケットで電子版を頒布しています。 BOOTHには物理本の在庫を復活しています。

書籍の詳細および販売サイトは次の記事を参照してください。 また、Amazon KindleおよびPOD版をリリース予定です*1

www.nowsprinting.com

なお、夏のC100版から若干変更が入っています。 Unity Test Frameworkパッケージv1.3で入った非同期テストに関する言及*2、Code Coverageパッケージv1.2で変更されたコマンドライン引数*3に対応しています。

Unity Test Frameworkパッケージ v1.3については次の記事を参照してください。

www.nowsprinting.com

Code Coverageパッケージ v1.2については公式のUpgrade Guideを参照してください。

docs.unity3d.com

また見本誌のみ置いていた『Unity Automated QA攻略ガイド』も、BOOTHおよび技術書典オンラインマーケットで電子版を頒布しています。 こちらはAutomated QAパッケージに開発再開の動きがないのでAmazon KindleおよびPOD版はペンディングです。

www.nowsprinting.com

所感

今回は暖かくて楽でした。一般参加者数はC100と同じだったそうですが、人の流れは若干少なく感じました。これは配置の問題ですね。

珍しく閉会までいたのですが、あまり撤収手伝えず離脱してしまいました。すみません。

C102予定

新刊が間に合っていればネタ切れで見送りの予定だったのですが、落としてしまったので申し込む方向です。 Unityの統合テスト(integration testing)にフォーカスした本になります。

ぜひ、サークル「いか小屋」をお気に入り登録してお待ち下さい。

*1:夏にも書きましたが、進捗悪くてすみません…

*2:元々付録にv2の非同期テストについて書いていたので、脚注程度

*3:よりわかりやすくなりました

Unity 2021.2未満でも動作するRoslyn Analyzerプロジェクト

Unity 2020.2から制限付きながらRoslynアナライザが動作するようになり、その後、Unity 2021.1.2で実用段階に行き着いたことは以前ブロク記事で紹介しました。

www.nowsprinting.com

しかし、この時点でUnityエディター上で動作しない*1アナライザがありました。例えば次のものです*2

これらは、Unity 2021.2.0以降では動作するようになりました。

結論

調査を放置していたのですが改めて確認してみたところ、結論としては

  • Unity 2021.2から内包するMicrosoft.CodeAnalysis.CSharpバージョンがv3.5からv3.9に変更された
  • Microsoft.CodeAnalysis.CSharpのバージョン3.6以降を使用してビルドされたアナライザはUnity 2021.2未満では動作せず、2021.2以降では動作する
  • Microsoft.CodeAnalysis.CSharpのバージョン3.10以降を使用してビルドされたアナライザはUnity 2021.2以降でも動作しない[11/14追記]

というものでした。 したがって、自作アナライザをUnity 2021.2未満にも対応させたいのであれば、使用するMicrosoft.CodeAnalysis.CSharpはv3.5に固定すればよいということになります*3

テンプレートプロジェクト

こちらのリポジトリで、Unity 2021.2未満でも動作するアナライザプロジェクトのテンプレートを公開しています。 テンプレート初期化の確認中にミスってリポジトリ消してしまったので、以前☆つけてくれていた方はまたお願いします…。

github.com

内包するRoslynバージョンの確認方法

以下おまけです。

Unityに内包されているRoslyn関連ファイルは、macOSでは下記パスにあります。

Unity 2021.2未満

/Applications/Unity/Hub/Editor/2021.1.28f1/Unity.app/Contents/Tools/Roslyn/Microsoft.CodeAnalysis.CSharp.dll

2021.1.28f1の部分はUnityエディタのバージョン

Unity 2021.2以降

/Applications/Unity/Hub/Editor/2021.3.4f1/Unity.app/Contents/Tools/ScriptUpdater/Microsoft.CodeAnalysis.CSharp.dll

この中のdllファイルを、Windowsであればilasm.exeというツールで見て確認できるはずです。 macOSの場合、Visual Studio for Macでも見られるようです。

参考:方法: ファイルがアセンブリであるかどうかを確認する | Microsoft Learn

今回はJetBrains Riderで読み込んで、Project Explorerでファイル右クリック | View in Assembly Explorerで確認しました。

*1:本稿での「動作しない」は、何ひとつコンソールに出力されずエラーメッセージも得られないもの

*2:全部検証しているわけではないので他にもあるでしょう。またバージョンによって振る舞いが異なる可能性もあります

*3:とはいえ、前述の通りUnity 2020 LTSのアナライザサポートには問題があるため、実質アナライザを活用していくのはUnity 2021 LTS以降ということで気にしなくてよさそうですが

Unity Automated QAのInput Systemサポートによる自動テスト

Unity Automated QAパッケージのRecorded Playback機能は、デフォルトではuGUI操作を記録・再生します。 しかし限定的ですが「Input Systemサポート」も提供されており、以下の操作に限り動作させることができ(ると書かれてい)ます。

  • UnityEngine.Input(従来のInput Manager)のみ*1
    • Supported Attributes: touches, touchCount, mousePosition
    • Supported Methods: GetTouch, GetMouseButton, GetMouseButtonDown, GetMouseButtonUp, GetKey, GetKeyDown, GetKeyUp, GetButton, GetButtonDown, GetButtonUp

Input Systemサポートについて、昨年末の同人誌『Unity Automated QA攻略ガイド』で調査途中と書いたままとなっていましたが、改めて調査してみました。 今さら感ありますが、そろそろホールドされているAutomated QAパッケージの開発が再開されるとされている時期なので期待も込めて。

www.nowsprinting.com

テスト対象の準備

Unity Asset Storeで無償提供されている2D Game Kit v1.9.5を使用しました。 選定理由は、New Input SystemおよびInput.GetAxis()*2を使っていないためです。

assetstore.unity.com

Unity 2022.1.20f1で開いたところ、UnityEngine.RuleTileがUnity.2D.Tilemap.Extras内にも定義されているというエラーが出たため、Assets/2DGamekit/Scripts/Utility/RuleTile.cs内のものをRuleTile2にリネームしています。

ここに、Automated QAパッケージをインストールします。 一覧には出てきませんが、 Package ManagerのAdd package by name...で"com.unity.automated-testing"を入力すればインストールできます。

記録のための設定

デフォルトではInput Managerによるキー入力は記録されません。 UnityエディタのメニューからAutomated QA | Automated QA Hub | Settingsを開き、Record Input Managerをonにします*3

再生のためのコード修正

続いて、キャラクター操作部分を少し書き換えます。 2D Game Kitでは、Inputから直接入力を取得しています。これにAutomated QAのPlaybackが介入できるよう、RecordableInputに置き換えます。

ファイルは、Assets/2DGamekit/Prefabs/EllenにアタッチされているAssets/2DGamekit/Scripts/Character/MonoBehaviours/InputComponent.csです。

次のような箇所を

Down = Input.GetButtonDown(k_ButtonsToName[(int)controllerButton]);

このように書き換えます*4

Down = RecordableInput.GetButtonDown(k_ButtonsToName[(int)controllerButton]);

Automated QAのアセンブリへの参照が必要です。 製品では、リリースビルドから除くため #if (UNITY_EDITOR || UNITY_INCLUDE_TESTS) を使うなどしたほうがいいでしょう*5

Automated QAパッケージにパッチを当てる

Input Managerの記録部分にバグがあり、パッチを当てる必要があります。 これは操作を記録する工程で必要なパッチであり、記録されたJSONファイルの再生時には無くても問題ありません。 言い換えると、再生だけならパッチを当てない素のパッケージを利用できます。

まず、パッチを当てるために、Automated QAパッケージを組み込みパッケージ化します。 プロジェクトディレクトリ下のLibrary/PackageCache/com.unity.automated-testing@0.8.1-preview.2を、Packagesディレクトリ下にコピーするだけです。

続いて、Packages/com.unity.automated-testing@0.8.1-preview.2/Runtime/RecordedPlayback/RecordingInputModule.csを開き、次の箇所を変更します。

internal void AddFullTouchData(TouchData td, bool setLastEventTime = false)
{
    playbackData.Add(td);  // ここを recordingData.Add(td); に書き換える

    if (setLastEventTime)
    {
        lastEventTime += td.timeDelta;
    }
}

Recorded Playback

以上の設定・修正を行った状態で、Automated QA | Automated QA Hub | Recorded Playbackを開き、Recordボタンで記録を開始します。 記録終了後に生成されるJSONファイルには、次のようにキー入力も記録されています。

Dキーを押して離した箇所の抜粋)

"touchData": [
{
    "pointerId": 0,
    "eventType": 5,
    "timeDelta": 1.9524928331375123,
    "position": {
        "x": 0.0,
        "y": 0.0
    },
    "positional": false,
    "scene": "Zone1",
    "button": 0,
    "waitSignal": "",
    "emitSignal": "",
    "keyCode": "D",
    "inputText": "",
    "inputDuration": 0.496268630027771,
    "objectName": "",
    "objectTag": "",
    "objectHierarchy": "",
    "objectIndex": 0,
    "querySelector": "",
    "objectOffset": {
        "x": 0.0,
        "y": 0.0
    }
},
{
    "pointerId": 0,
    "eventType": 9,
    "timeDelta": 0.0,
    "position": {
        "x": 0.0,
        "y": 0.0
    },
    "positional": false,
    "scene": "Zone1",
    "button": 0,
    "waitSignal": "",
    "emitSignal": "",
    "keyCode": "d",
    "inputText": "",
    "inputDuration": 0.496268630027771,
    "objectName": "",
    "objectTag": "",
    "objectHierarchy": "",
    "objectIndex": 0,
    "querySelector": "",
    "objectOffset": {
        "x": 0.0,
        "y": 0.0
    }
},

このファイルを指定して再生すると、キー入力による左右移動やジャンプが再現されました!

なお、マウスクリックも記録されますが、"keyCode": "Fire1" の入力として記録され、座標は常に { 0, 0 } となりますので注意してください。

Test Generation

続いてテストコード生成を試します。 3種類の生成方法がありますが*6、"Full Tests"の場合は"Use Simplified Driver Code"をオフにしたほうがよさそうです。

"Full Tests"かつ"Use Simplified Driver Code"がオンの場合、再生がスムーズでなくなります(はじめのほうのフレームが無視されている?)。

"Use Simplified Driver Code"がオフ、もしくは"Simple Tests"であれば、スムーズに再生されました。

所感

PCやコンソール向けの場合、素直にNew Input Systemを使うほうが自動化は楽なはずです。 また、Automated QAパッケージの開発が再開されたとして、レガシーなInput Managerのサポートを続けるのかは謎です。 したがって、この記事が役に立つことはない気がしますが、供養のため公開します。

*1:であればInput Systemと呼ばないでほしいところですが、ソースを見るとNew Input Systemもサポートしようとする形跡が見えます

*2:Input Systemサポート対象外です。このため3Dゲームのサンプルにはほぼ適用できません

*3:ドキュメントに書かれていなくて忘れがち

*4:ゲームコントローラ向けに一箇所だけGetAxisRaw()があります。これはサポート外なので、RecordableInputに置き換えても再生されません。なお、未サポートのメソッドはUnityEngine.InputにフォールバックされていますのでInputとしての振る舞いはキープされます

*5:ちゃんとビルドからストリップされるかは要検証

*6:詳しくは『Unity Automated QA攻略ガイド』参照