やらなイカ?

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

Unity Test Frameworkで利用できるカスタム属性の実装方法

Unity Test Framework および NUnit では、テストコードに付与できるさまざまな属性(attribute)が提供されています。 また拡張ポイントであるインターフェイスを実装することで、プロジェクト独自のカスタム属性を定義してテストコードで使うこともできます。

本記事では、オープンソースの Test Helper パッケージにいくつかの汎用的なカスタム属性を実装するにあたって得た知見を紹介します。

github.com

本記事で紹介するインターフェイスは次の4つです。 先頭から3つが NUnit 由来のもの、最後のひとつが Unity Test Framework 独自のものです。

検証環境は次のとおりです。

  • Unity 2022.3.11f1 *1
  • Unity Test Framework v1.3.4 *2

カスタム属性実装の基本

カスタム属性は次のように定義します。 NUnitAttribute クラスを継承し、役割に応じたインターフェイスを実装(ここでは IApplyToContext)しています。

public class MyAttribute : NUnitAttribute, IApplyToContext
{
    private readonly string _message;

    public MyAttribute(string message) => this._message = message;

    public void ApplyToContext(ITestExecutionContext context)
    {
        Debug.Log(message);
    }
}

この属性を次のようにテストメソッドに付与すると*3、テストメソッドの実行直前に Test start! とログ出力されます。

[TestFixture]
public class MyTestClass
{
    [Test]
    [MyAttribute("Test start!")]
    public void MyTestMethod()
    {
        Assert.That(actual, Is.EqualTo(expected));
    }
}

インタフェースの解説

4つのインターフェイスの使用方法を順に解説します。

IApplyToTest

IApplyToTest インターフェイスを実装した属性は、テストランナーが実行するテストを決めるときに ApplyToTest メソッドが呼ばれます。 ここでテストコンテキストの RunState を変化させることで、テストの実行可否を制御できます。

次のコードは、付与されたテストを実行から除外するカスタム属性の例です。

public class MyIgnoreAttribute : NUnitAttribute, IApplyToTest
{
    void IApplyToTest.ApplyToTest(Test test)
    {
        test.RunState = RunState.Ignored; // RunStateを変更して実行されないようにする
    }
}

Unity Test Framework の Category 属性、Ignore 属性などはこのインターフェイスを実装しています。

Test Helperパッケージでは、次のカスタム属性で使用しています。

実装にあたっては、次の点に注意してください。

  • 属性の中で例外を発生させてはいけません。Test Runnerウィンドウで(スキップでなく)エラーと表示され、テスト実行結果の件数からも除外されます*4
  • 実行中のテストコンテキストを NUnit.Framework.TestContext から取得できません。引数で渡される test インスタンスを使います
  • テストメソッド実行直前だけでなく、Test Runnerウィンドウの表示更新の契機で呼ばれます。そのため、プロジェクトの設定を一時的に変更するといった用途では使用できません(変更を戻す契機がないため)

IApplyToContext

IApplyToContext インターフェイスを実装した属性は、付与されたテストの実行前に ApplyToContext メソッドが呼ばれます。 引数で渡されるテストコンテキストに変更を加えたり、何かしらの前処理を実行できます(ただし同期処理に限る)。

次のコードは、付与されたテスト実行直前にテスト名称をログ出力するカスタム属性の例です。

public class MyLoggingAttribute : NUnitAttribute, IApplyToContext
{
    public void ApplyToContext(ITestExecutionContext context)
    {
        Debug.Log($"{context.CurrentTest.Name} start!");
    }
}

Unity Test Framework の Timeout 属性はこのインターフェイスを実装しています。

Test Helperパッケージでは、次のカスタム属性で使用しています。

実装にあたっては、次の点に注意してください。

  • 属性の中で例外を発生させてはいけません。テストが無限ループします
  • 実行順は、OneTimeSetUpより後、UnitySetUp および SetUp よりも前です

ICommandWrapper

ICommandWrapper は直接実装するのではなく、サブインターフェイスである IWrapSetUpTearDown もしくは IWrapTestMethod を実装します。

テスト実行の前後に処理を行なうための属性ですが、Unityでは次の制限事項があります。 重めの問題があるため、Unityでは後述の IOuterUnityTestAction を使用することをおすすめします*5

  • テストメソッドに付与したときしか動作しません(テストクラス、テストアセンブリはNG)
  • 同期処理しか書くことができません
  • 属性の中で例外を発生させてはいけません。テストが無限ループします
  • 非同期テスト(asyncおよびUnityTest)に付与すると、実行時エラーや無限ループを引き起こします
  • IWrapSetUpTearDown を付与したテストに async SetUp メソッドがあると、無限ループを引き起こします
  • IWrapSetUpTearDown の実行順は、OneTimeSetUp および UnitySetUp より後、SetUp よりも前です *6
  • IWrapTestMethod の実行順は、SetUp の後、テストメソッドの前です
  • 引数で渡される TestCommand から取得できるテストコンテキストは、テストケースではなくテストフィクスチャ(テストクラス)のものです

Unity Test Framework の MaxTime 属性、Repeat 属性、Retry 属性は IWrapSetUpTearDown を実装しています。つまり上記の問題があるということです。 詳しくは次の記事を参照してください。

www.nowsprinting.com

IOuterUnityTestAction

IOuterUnityTestAction は、NUnit由来のものでなくUnity独自のインターフェイスです。 このインターフェイスを実装した属性は、付与されたテストの実行前に BeforeTest メソッド、実行後に AfterTest メソッドが呼ばれます。

BeforeTestUnitySetUp より前に呼ばれる、例外を投げても無限ループしないでテストが失敗してくれるなど、とても高品質なインターフェイスです*7

次のコードは、付与されたテスト前後にテスト名称をログ出力するカスタム属性の例です。

public class MyLoggingAttribute : NUnitAttribute, IOuterUnityTestAction
{
    public IEnumerator BeforeTest(ITest test)
    {
        Debug.Log($"{test.Name} start!");
        yield return null;
    }

    public IEnumerator AfterTest(ITest test)
    {
        if (TestContext.CurrentTestExecutionContext.CurrentResult.PassCount > 0)
        {
            Debug.Log($"{test.Name} success!");
        }
        else
        {
            Debug.Log($"{test.Name} failure!");
        }
        yield return null;
    }
}

Test Helperパッケージでは、次のカスタム属性で使用しています。

以下、補足です。

  • テストメソッドに付与したときしか動作しません(テストクラス、テストアセンブリはNG)
  • 実行順は、OneTimeSetUp -> BeforeTest -> UnitySetUp -> SetUp -> Test -> TearDown -> UnityTearDown -> AfterTest -> OneTimeTearDown
  • BeforeTest 内で何かしらの例外をスローすると、テスト本体は実行されず失敗扱いとなります
  • AfterTestは、テストの成否に関わらず必ず実行されます
  • AfterTest 内で例外をスローすると、テスト本体が成功していてもテストを失敗させることができます
  • AfterTest 内でテストの成否を判定するには、上例のようにテストコンテキストの PassCountFailCount を参照します

カスタム属性を実装するにあたってのTips

いくつか紹介します。

属性を付与できるシンボルを限定したい

属性の定義に AttributeUsage 属性を付与することで、属性を付与できるシンボルを限定できます。 例えばメソッドにしか付与できないようにするには、次のように書きます。

[AttributeUsage(AttributeTargets.Method)]
public class MyAttribute : NUnitAttribute, IOuterUnityTestAction {}

属性が付与されたシンボルを検索したい

テストに限らず、属性が付与されたシンボルを収集して処理したい場合、Unityエディター内ではリフレクションでなく TypeCache クラスのメソッドを使うと高速に処理できます。

次のコードは、LoadSceneAttribute が付与されたメソッドを返すメソッドの例です。

private static IEnumerable<LoadSceneAttribute> FindLoadSceneAttributesOnMethods()
{
    var symbols = TypeCache.GetMethodsWithAttribute<LoadSceneAttribute>();
    foreach (var attribute in symbols
                 .Select(symbol => symbol.GetCustomAttributes(typeof(LoadSceneAttribute), false))
                 .SelectMany(attributes => attributes))
    {
        yield return attribute as LoadSceneAttribute;
    }
}

テストから属性に情報を渡したい

属性のコンストラクタに引数を定義できます。 本記事の最初のコード例を参照してください。

なお、上例では属性のフィールドに値を保持していますが、テストコンテキストの Properties に保持することもできます(後述)。

また NUnitAttribute の代わりに PropertyAttribute も利用できます。

属性からテストに情報を渡したい

属性の処理内で得た何かしらの情報をテストメソッドに渡したい場合、テストコンテキストの Properties を使用できます。 次のように使用します。

情報のセット

public class PassesObjectAttribute : NUnitAttribute, IApplyToContext
{
    public void ApplyToContext(ITestExecutionContext context)
    {
        MyClass obj = // snip

        if (obj != null)
        {
            test.Properties.Add("PassesMyClass", obj); // nullをセットしないよう注意
        }
    }
}

情報の取り出し

[TestFixture]
public class MyTestClass
{
    [Test]
    [PassesObject]
    public void MyTestMethod()
    {
        var obj = TestContext.CurrentTestExecutionContext.CurrentTest
            .Properties.Get("PassesMyClass") as MyClass;

        Assert.That(obj, Is.Not.Null);
    }
}

なお、プロパティに null をセットしてしまうと、バッチモードでテスト実行したときにテスト全体がエラーとなります。 テストレポート出力時に次の例外を処理されていないためです。

Uploading Crash Report
NullReferenceException: Object reference not set to an instance of an object
  at NUnit.Framework.Internal.PropertyBag.AddToXml (NUnit.Framework.Interfaces.TNode parentNode, System.Boolean recursive) [0x00054] in <956b82cfdef641c6bc6a0e5b19798f05>:0 
(snip)

その他の Unity Test Framework 拡張ポイント

本記事で紹介したカスタム属性以外にも、カスタム Comparer やカスタム制約といった拡張が可能です。 詳しくは『Unity Test Framework完全攻略ガイド』を参照してください。

www.nowsprinting.com

*1:テストは 2019 LTS, 2020 LTS, 2021 LTS, および 2023.1.16f1 でも実行しています

*2:正常系は最新のv1.3.9でも動作しますが、異常系の振る舞いが異なります

*3:テストクラス、テストアセンブリにも付与できます

*4:Test Frameworkのバグですが修正されないでしょう

*5:ただし IOuterUnityTestAction は IWrapSetUpTearDown の代替になりますが、IWrapTestMethod に代わるインターフェイスはありません

*6:UnitySetUp はほかにもドメインリロードで再実行されないなどの癖があります。どうして…

*7:冷静に考えると嫌味っぽいですが、検証したとき正直感動したのです…

Unity製ゲームをWebGLビルドしてGitHub Pagesに公開するワークフロー

Unityプロジェクトのバージョン管理にGitHubを使用している場合、GitHub ActionsワークフローでWebGLビルドを行ない、そのままGitHub Pagesに公開する方法を紹介します。

GitHub Pagesは、GitHubリポジトリに紐づく静的コンテンツをホスティングできる機能です。 公開できるサイズが10GB以下ということと、たとえprivateリポジトリであってもGitHub Pagesにはアクセス制限はかけらない*1という制限がありますので注意してください。

Unity - Builder アクション

ビルドには、GameCIの提供するUnity - Builder アクションを使用しています。 利用方法は公式サイトか、検索すれば日本語情報もありますので割愛します。

github.com

なお、CIをキックするユーザーはUnityのライセンスを持っている必要があります*2。publicなリポジトリで使用する場合は特に注意してください*3

また、現在パーソナルライセンスのCLIによる新規アクティベーションは塞がれています。次のIssueを参照してください。

alf->ulf license activation no longer possible for personal licenses · Issue #408 · game-ci/documentation · GitHub

GitHub Pages

GitHub Pagesで公開するコンテンツは、リポジトリの特定ブランチにあらかじめチェックインするか、GitHub Actionsによってアーティファクトにアップロードしたものを使用できます。

後者であれば、リポジトリにビルド成果物をチェックインする必要がなく便利です。 本例ではこの方法を紹介します。

まず、GitHubリポジトリSettings > Pagesを開き、SourceはGitHub Actionsを選択します。

すると、リポジトリのEnvironmentsにgithub-pagesが追加されます。 あとはワークフローで、WebGLビルド出力をtarで固めてgzipしたものをgithub-pagesという名前でアーティファクトにアップロードすることで、動作するゲームが https://OWNER.github.io/REPO/ で公開されます。

なお、GitHub Pagesで公開するWebGLは、Compression FormatをDisabledに設定しなければなりません。公開は行われますが実行時に次のエラーが出ます。

Unable to parse Build/WebGL.framework.js.gz! This can happen if build compression was enabled but web server hosting the content was misconfigured to not serve the file with HTTP Response Header "Content-Encoding: gzip" present. Check browser Console and Devtools Network tab to debug.

しかし、ゲームをunityroomで公開する場合、Compression FormatはGzipに設定する必要があります。 これを手動で切り替えるのは事故の元ですので、ワークフロー内でビルド前に書き換えています。

詳しくは、下記実装例を参照してください。

ワークフロー実装例

次のワークフローをリポジトリの.github/workflows下に置くことで、masterブランチにpush(もしくはPRをマージ)されるたびにWebGLビルドが実行され、GitHub Pagesで公開されます。

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - master
  # masterブランチにpushされるたびにWebGLビルドして公開。条件はプロジェクトのニーズによって調整してください

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
  # 先行ジョブがあればキャンセル

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Set CompressionFormat to Disabled
        run: |
          sed -i -e 's/webGLCompressionFormat: ./webGLCompressionFormat: 2/' ProjectSettings/ProjectSettings.asset
        # Compression Formatを`Disabled`に書き換え

      - uses: game-ci/unity-builder@v3
        with:
          targetPlatform: WebGL
          allowDirtyBuild: true # 上でProjectSettings.assetを変更しているため指定
        env:
          UNITY_LICENSE: ${{ secrets[UNITY_LICENSE_2022] }}

      - uses: actions/upload-pages-artifact@v2
        with:
          path: build/WebGL/WebGL
        # WebGLビルド出力をアーティファクトにアップロード

  deploy:
    runs-on: ubuntu-latest
    needs: build
    permissions:
      id-token: write
      pages: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}

    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2
        # GitHub Pagesに公開

*1:GitHub Enterprise契約であればできます

*2:Proライセンスが必要なプロジェクトであれば全員Proライセンスが必要です

*3:public forkからのPull Requestの実行を抑止することで不特定ユーザーによるキックを防ぐことはできます。ただしこれもUnity社のお墨付きというわけではないので保証の限りではありません

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

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

頒布した書籍の詳細・フォローアップは次の記事を参照してください。

www.nowsprinting.com

www.nowsprinting.com

いずれも、BOOTHで電子版および物理本を頒布しています。買いそびれた方はご利用ください。

ikagoya.booth.pm

またTAROMAN本はメロンブックスさんにも委託あります。 https://www.melonbooks.co.jp/detail/detail.php?product_id=2037143

今回、強気で多めに刷ったので、だいぶ在庫あります。 強気の分だけ丸々残っている感。 Unity本などと比べて情報が風化しにくいはずなので、しばらく既刊として持っていくつもりです。

ところで、今回はポスターも用意しましたが、やっぱり縦に使うべきだったかな、という反省。

テスト自動化で大切なことはTAROMANが教えてくれた

テスト自動化のノウハウ・ベストプラクティスを岡本太郎の言葉で紐解く、そんな同人誌をコミックマーケット102の2日目(日曜)西お40b「いか小屋」で頒布します。

想定読者

  • テスト自動化エンジニア及びSET
  • 自動テストの導入を検討しているプロジェクトのオーナー、マネージャー、 QA 担当者(主に第1章「テスト戦略編」)
  • ソフトウェアエンジニア、プログラマ(主に第2章「テスト実装編」)

目次の紹介

各章の内容は次のとおりです。

第1章 テスト戦略編

『TAROMAN』各話タイトルとなっているものを中心に、主にテスト自動化に取り組む際の戦略・計画に役立つ岡本太郎氏の言葉を紹介しています。

  • でたらめをやってごらん
  • 自分の歌を歌えばいいんだよ
  • 一度死んだ人間になれ
  • 同じことをくりかえすくらいなら、死んでしまえ
  • 人生は他人を負かすためにあるのではない
  • 目的を捨ててごらん

第2章 テスト実装編

『TAROMAN』各話タイトルとなっているものを中心に、主に自動テストを実装する際に役立つ岡本太郎氏の言葉を紹介しています。

  • 真剣に、命がけで遊べ
  • マイナスに飛び込め
  • 美ってものは、見方次第なんだよ
  • 捨てる主義のすすめ
  • 好かれるヤツほどダメになる
  • 孤独こそ人間が強烈に生きるバネだ
  • なま身の自分に賭ける
  • 芸術は爆発だ

なお、1・2章で、2022年に再放送された『TAROMAN』全10話のタイトルを網羅しています*1。 普段テスト自動化に携わっている方なら7-8割方は何を書いているか予測がつくと思いますが、さすがに苦しいこじつけもあります。ゆるして…

第3章 「タローマンと 3 つのちかい」と「テスト自動化の 8 原則」

テスト自動化の8原則のベースとなったとされる「タローマンと3つのちかい」。両者の関連を解説しています。

  • うまくあるな
  • きれいであるな
  • ここちよくあるな

付録A オープンソース開発で大切なことも岡本太郎が教えてくれた

『TAROMAN』とは関係ない岡本太郎の言葉の中から、オープンソース開発につながる言葉を紹介しています。

付録B 真実の「メインフレーム時代の自動テスト」

本書は「岡本太郎はテスト自動化エバンジェリストだった」というテイで書かれています。 『TAROMAN』の放映された1970年代はメインフレームの時代ですが、実際にメインフレームのプロダクト開発で行われていた*2テスト自動化について簡単に紹介しています。

表紙について

ヤサカナエさん(サークル「あまどや」さん)にお願いしました。 色々アイデア出していただいてありがたかったです。最終的に「勢いのあるやつ」に。

スペース

2日目(日曜)西お40b「いか小屋」でお待ちしております。 西2手前側の入り口が最寄り。

技術書典14新刊『Unity Test Framework完全攻略ガイド 統合テスト編』の在庫も持っていきます。

www.nowsprinting.com

信販売・電子版について

C102後、BOOTHおよびメロンブックスさんに委託予定です。 二次創作なので技術書典およびKDPには出しません。

メロンブックスさん予約はじまっています → https://www.melonbooks.co.jp/detail/detail.php?product_id=2037143

BOOTHでも頒布開始しました [8/14]

ikagoya.booth.pm

参考

*1:入稿後にNHKで『捨てる主義のすすめ』が復刻再放送されたようですが、子供の頃に見たテイで書いています

*2:1970年代ではなく1990年代の話ですが

Unityバイブル R5夏号

8月29日に発売される『Unityバイブル R5夏号』の予約受付中です。Amazonに書影も出ました。

鈍器とか質量兵器とか言われた『Unityゲーム プログラミング・バイブル 2nd Generation』とは異なり、1冊のボリュームを抑えて年2冊発行されるようになったそうです。

掲載されているのは次の10トピックです。

【特集記事】

  • 高品質なグラフィックスを表現するためのUniversal RPの各種機能の解説
  • Shader Graphの基本操作から発展的なテクニックまで
  • Timelineを利用してリッチな演出を作成する手順や考え方
  • Photon Fusionによるリアルタイムネットワークゲーム
  • Unity Localizationを利用したシンプルな多言語化

【一般記事】

  • ゲームプレイの自動テスト
  • Unity開発を効率的にする.NET向けIDE「JetBrains Rider」の活用
  • Editor拡張で作業効率を上げよう
  • すぐに使えるゲームサウンド演出
  • VRアプリケーションにおけるVRMアバターの導入

このうち「ゲームプレイの自動テスト」のセクションを執筆させていただきました。商業出版も共著も、実に10年ぶりでした。

ゲームプレイの自動テスト

扱っている範囲は同人誌『Unity Test Framework完全攻略ガイド 統合テスト編』と同じく統合テスト中心ですが、同人誌では統合テストの手法について広く書いているのに対し、本書は実際に動作するゲームに対してテストを書いていく、より実践的な内容となっています*1

サンプルゲームはローグライクのダンジョン探索ゲームです。 あえて*2無計画に作りはじめて、テストがあればいくら変更しても大丈夫とばかり、機能追加、仕様変更、リファクタリングを何度も重ねた過程がコミットログから読みとれるはずです。 プロジェクトは本書発売に合わせて公開予定です。

本文では、主に次の内容を紹介しています。

  • ダンジョン自動生成ロジックのテスト
    • マップ生成のテスト
    • 3D要素配置のテスト
  • インゲームのテスト
    • InputSystem.TestFrameworkを使用したテスト実装
    • インゲームのモンキーテスト
    • インゲームのシナリオテスト(パッド/キー操作のキャプチャ/プレイバック)
  • アウトゲームのテスト
    • UI要素を操作するテスト
    • アウトゲームのモンキーテスト
    • アウトゲームのシナリオテスト(Automated QAパッケージによるキャプチャ/プレイバック)

ぜひご予約の上、発売をお待ち下さい!*3

物理本のほか、ボーンデジタル オンラインブックストアでPDF版を購入できます。 [8/22]

www.borndigital.co.jp

サンプルプロジェクト

サンプルプロジェクトを公開しました [8/22]

github.com

関連

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

*1:実践的な反面、網羅性はなくなっています。ぜひ『Unity Test Framework完全攻略ガイド 統合テスト編』と合わせてお読みください

*2:本当ですよ?

*3:CEDEC2023で先行販売があるそうです

PlayModeWindowでGameビューの解像度を指定する

PlayModeWindowは、Unity 2022.2で追加されたクラスです。 従来、UnityエディターのGameビューをスクリプトから操作するにはリフレクションによる非公開APIの使用が必要でした。 Unity 2022.2からは、PlayModeWindowクラスの静的メソッドで操作できます。

PlayModeWindowクラスは、PlayModeViewクラス*1を制御する機能を持ち、GameビューとSimulatorビューを切り替えたり、解像度を変更するAPIが提供されています。

docs.unity3d.com

Gameビューへの切り替え

解像度の指定は、SimulatorビューではなくGameビュー表示である必要があります。 次のように切り替えます。

PlayModeWindow.SetViewType(PlayModeWindow.PlayModeViewTypes.GameView);

引数には、GameViewもしくはSimulatorViewを指定できます。

解像度の指定

解像度はSetCustomRenderingResolutionメソッドで変更できます。 Full HD(1920×1080)に変更する場合、次のように指定します。

PlayModeWindow.SetCustomRenderingResolution(1920, 1080, "Full HD");

テスト実行時の解像度を固定する方法1(ICallbacks)

たとえば、すべてのテストで解像度をFull HD(1920×1080)に固定したければ、ICallbacksを使用して次のように実装します。

public class OpenGameViewAtRunStarted : ICallbacks
{
  [InitializeOnLoadMethod]
  private static void SetupTestCallbacks()
  {
    var api = ScriptableObject.CreateInstance<TestRunnerApi>();
    api.RegisterCallbacks(new OpenGameViewAtRunStarted());
  }

  public void RunStarted(ITestAdaptor testsToRun)
  {
    PlayModeWindow.SetViewType(PlayModeWindow.PlayModeViewTypes.GameView);
    PlayModeWindow.SetCustomRenderingResolution(1920, 1080, "Full HD");
  }

  // snip
}

テスト実行時の解像度を固定する方法2(GameViewResolution属性)

テスト実行時に解像度指定する場合は、Test Helperパッケージに含まれるGameViewResolution属性が便利です。

この属性はテストアセンブリ・クラス・メソッドいずれかに付けることができ、どこにつけても各テストメソッドの実行前(SetUpの前)に処理されます。

すべてのテストで同じ解像度でよければ、次のようにテストアセンブリに付与します*2

[assembly: GameViewResolution(1920, 1080, "Full HD")]

テストごとに個別に指定したいときには、次のようにテストクラスやメソッドに付与できます。

[TestFixture]
public class MyTestClass
{
  [UnityTest]
  [GameViewResolution(1920, 1080, "Full HD")]
  public IEnumerator MyTestMethod()
  {
    // test
  }
}

Test Helperパッケージは、Unity 2019.4以降で利用できます。 Unity 2022.2未満のときはリフレクションによる非公開APIを使用していますので、自分で実装するときの参考にもなるはずです。

github.com

補足

本記事は、拙著『Unity Test Framework完全攻略ガイド 統合テスト編』の第5章「ビジュアルリグレッションテスト」からの抜粋です。

www.nowsprinting.com

テストの基本から知りたい! という方は、『Unity Test Framework完全攻略ガイド 第2版』からお読みください!

www.nowsprinting.com

*1:GameViewクラスおよび、旧Device SimulatorパッケージのSimulatorWindowクラスの親クラス。Device SimulatorがUnity 2021.1からビルトインされて変更されましたが、概念としては変わらず2つのビューの抽象

*2:Play Modeテストアセンブリが複数あるプロジェクトであれば、アセンブリごとに指定が必要です

バッチモードでUnityを実行するときの制限事項と回避方法

本記事は、拙著『Unity Test Framework完全攻略ガイド 統合テスト編』の付録Bから抜粋したものです。

バッチモードとは

バッチモードとは、Unityエディターをコマンドラインから起動するときに -batchmode オプションを付けることでヘッドレス動作するモードです。 ウィンドウモード*1のUnityエディターは実行中さまざまな、人間が応答しなければならないポップアップウィンドウが表示されますが、バッチモードでは表示されません。そのため、CIでテスト実行するときなどに使用されます。

バッチモードの制限事項

本記事執筆時点で判明している、バッチモード実行時の制限事項は次のとおりです。

  • スクリーンサイズは640×480
  • WaitForEndOfFrame を使用できない*2
  • InputSystem.TestFramework を長時間(数十秒)動作させたとき、途中で操作が止まってしまう
  • UnityEngine.InputSystem.LowLevel.InputEventTrace を使用できない
  • -testPlatform オプションでスタンドアロンプレイヤーを指定したとき、バッチモードでなく常にウィンドウモードで起動する

制限事項の回避方法1

バッチモード実行中であっても、EditorWindow.GetWindow() メソッドでUnityエディターのGameビューを開くことで前述の制限を回避できます。

ヘッドレスとは言ったがウィンドウを表示できないとは言っていない

とのこと。

次のコードは、テスト実行前にGameビューを開く ICallbacks の実装例です。

public class OpenGameViewAtRunStarted : ICallbacks
{
  [InitializeOnLoadMethod]
  private static void SetupTestCallbacks()
  {
    var api = ScriptableObject.CreateInstance<TestRunnerApi>();
    api.RegisterCallbacks(new OpenGameViewAtRunStarted());
  }

  public void RunStarted(ITestAdaptor testsToRun)
  {
    var assembly = Assembly.Load("UnityEditor.dll");
    var viewClass = Application.isBatchMode
        ? "UnityEditor.GameView"
        : "UnityEditor.PlayModeView";
    var gameView = assembly.GetType(viewClass);
    EditorWindow.GetWindow(gameView, false, null, true);
  }

  // snip
}

GameView および親クラスである PlayModeView は可視性が internal のため、リフレクションを使用しています。そのため、今後Unityエディターの内部実装変更によって動作しなくなる恐れがあります。

ただし、グラフィックボード・ビデオカードを搭載していないマシン、もしくは -nographics オプションを指定しているときには、EditorWindow.GetWindow() メソッドは機能しません。 エラーにはなりませんがウィンドウも開きません。

なお、当然ですが本当にGameビューが開きます。開発に使用しているPCで動作させたとき、フォーカスを持っていかれたり、そのままキーボード操作がテストに干渉する恐れがありますのでご注意ください。

制限事項の回避方法2

下記リポジトリで公開しているTest Helperパッケージに含まれる FocusGameView 属性も同様の実装をしています。 したがって、この属性をテストアセンブリ・クラス・メソッドいずれかに付けるだけでバッチモードの制限事項を回避できます。

また、Gameビューの解像度を指定する GameViewResolution 属性も同じ処理を含みますので、こちらでも回避できます。

github.com

まとめ

いかがでしたでしょうか?(構文)

この記事が役にたった! という方は、ぜひ『Unity Test Framework完全攻略ガイド 統合テスト編』をお買い求めください!

www.nowsprinting.com

テストの基本から知りたい! という方は、『Unity Test Framework完全攻略ガイド 第2版』からお読みください!

www.nowsprinting.com

*1:便宜上、通常のGUIで動作するUnityエディターを指します。公式の名称ではありません

*2:https://docs.unity3d.com/Manual/CLIBatchmodeCoroutines.html