Unityでテストを書いてCircleCIでコミットのたびにテストをチェックする

はじめに

タイトルのとおり、Unityでテストを書いて CircleCIでコミットのたびにテストをチェックするようにしました。

  • Unity: 2019.1.14f1

テストを書く

Unity標準で用意されているTest Runnerを利用します。

まずはAssets/Tests/ディレクトリを作成します。

2019 08 22 16 55 35

「Create > Testing > Test Assembly Folder」を選択します。

2019 08 22 16 55 52

作成されるディレクトリを「PlayMode」と名付けます。

2019 08 22 16 57 57

「Window > General > Test Runner」からTest Runnerの画面を開きます。

2019 08 22 16 59 37

「Create EditMode Test Assembly Folder」を実行します。

2019 08 22 17 00 50

EditModeとPlayModeでそれぞれ 「Create Test Script in current folder」をクリックします。

2019 08 22 17 01 04

2019 08 22 17 02 52

EditModeとPlayModeの両方でRun Allを実行して テストが通ることを確認します。

2019 08 22 17 03 31

今回はサンプルのテストコードをそのまま利用しています。 実際のテストコードを実際に書く場合にはAssembly Definitionの設定などが必要になります。

UnityのTest Runnerでusingが効かなかった - Qiita

UnityでMV(R)P+Zenjectでテストを行いマルチシーンも試してみる | 測度ゼロの抹茶チョコ

UnityのプロジェクトをGitHubにアップロードする

GitHubでリポジトリを作成します。 Unity用の.gitignoreが用意されているので使いましょう。

2019 08 23 21 54 27

「Project Settings > Editor > Version Control > Mode」を 「Visible Meta Files」にします。

2019 08 22 17 14 53

UnityのプロジェクトをGitHubに上げる際はGit LFSを考えたほうがよいでしょう。

GitHubの無料枠でGit LFSはアカウントに付き1GBまで利用できます。 月$5で容量を50GBずつ増やせるようです。

Git LFSの設定をしたらgit pushでUnityのプロジェクトをGitHubにアップロードします。

CircleCIでUnityのテストを動かす

CircleCIをコンテナベースで使っていきます。 UnityのコンテナイメージはgablerouxさんがDockerHubにgableroux/unity3d/tagsで公開してくれています。 Unityの本体は/opt/Unity/Editor/Unityにあります。

Unityのコマンドライン引数はマニュアル(Unity - Manual: Command line arguments)があります。 オプション-batchmodeで実行します。

コマンドライン引数のマニュアルには書いていませんが、Unity Test Runnerのマニュアルにはテスト用のコマンド引数が載っています。 わかりにくいのでコマンドライン引数のところにまとめて書いておいてほしい。

-runTestsの追加でテストを実行できます。 -testPlatform editmodeの追加で エディットモードのテストができます。 -testPlatform playmodeの追加で プレイモードのテストができます。 -testsResultFile result.xmlで 出力ファイル名を変更できます。

と、いうことでデフォルトのUnityでもコマンドラインからテストは実行できるのですが。 なぜか私の手物の環境ではPlayModeのテストがexit code 134で異常終了してしまいました。 また、それ以外にもPlayModeテストには残念な部分があって、 テストが失敗してもexit codeは成功になりresult.xmlで判断するという設計だそうです。 CIを利用するにあたっては終了コードで結果がわからないと緑色になってしまいます。

そこでRuntimeUnitTestToolkitを利用します。

このツールはUnity Test Runnerのコードをそのまま使って CIでも使いやすい結果を返してくれるものです。

エディタ上では普通のUnity Test Runnerを利用しつつ、 CIのPlaymodeテストではRuntimeUnitTestToolkitを利用します。

Releases · Cysharp/RuntimeUnitTestToolkitからunitypackageをダウンロードして導入します。

また、CIではUnityのライセンス確認が少し厄介です。 ライセンス用のファイルがあるのでそれを利用します。

まずは手元のdockerで次のコマンドを実行します。

docker run -it gableroux/unity3d:2019.1.14f1 bash
/opt/Unity/Editor/Unity -quit -batchmode -nographics -logFile -createManualActivationFile
cat Unity_v2019.1.14f1.alf

Unityv2019.1.14f1.alfというのがライセンスファイルの元となるxmlです。 catしてコピー&ペーストしてUnityv2019.1.14f1.alfという名前で保存します。

次にこのalfファイルをUnity - Activationにアップロードします。 するとulfファイルがもらえます。 これはパスワードなどが入っているのでGitリポジトリにはアップロードせず、 CircleCIの環境変数で渡します。 xmlファイルをそのままコピー&ペーストするとうまく行かないので base64をかけて使う側でbase64をデコードして使います。

cat Unity_v2019.x.ulf | base64

このコマンドの結果をメモっておきます。

Gitリポジトリのトップに.circleci/config.ymlを作ります。

version: 2.1
executors:
  unity:
    parameters:
      unity_version: { type: string }
    docker:
      - image: gableroux/unity3d:<< parameters.unity_version >>
commands:
  unity_activation:
    parameters:
      unity_license: { type: string }
    steps:
      - checkout
      - run:
          name: decode license
          command: echo << parameters.unity_license >> | base64 --decode >> .circleci/Unity.ulf
      - run:
          name: activate unity license
          command: /opt/Unity/Editor/Unity -batchmode -nographics -manualLicenseFile .circleci/Unity.ulf || exit 0
  test-editmode:
    steps:
      - run:
          name: Edit mode test
          command: /opt/Unity/Editor/Unity -batchmode -nographics -projectPath . -runEditorTests -testPlatform editmode -editorTestsResultFile test-results/results.xml
      - store_artifacts:
          path: test-results/results.xml
  test-playmode:
    steps:
      - run:
          name: Build Linux(Mono)
          command: /opt/Unity/Editor/Unity -quit -batchmode -nographics -projectPath . -executeMethod UnitTestBuilder.BuildUnitTest /headless /ScriptBackend Mono2x /BuildTarget StandaloneLinux64
      - run:
          name: Play mode test
          command: bin/UnitTest/StandaloneLinux64_Mono2x/test
jobs:
  test-job:
    parameters:
      version: { type: string }
      license: { type: string }
    executor:
      name: unity
      unity_version: << parameters.version >>
    steps:
      - unity_activation: { unity_license: << parameters.license >> }
      - test-editmode
      - test-playmode
workflows:
  version: 2
  test-workflow:
    jobs:
      - test-job:
          version: 2019.1.14f1
          license: ${UNITY_LICENSE_2019}

Unityがライセンスの認証が成功したときでも exit codeが1という謎の挙動をするので|| exit 0で上書きしています。

unitypackageの追加と.circleci/config.ymlをコミットしておきます。

次にGitHubのリポジトリとCircleCIの連携を行います。 CircleCIの「Add Projects」からGitHubのリポジトリを選んでSet Up Projectをクリックします。

2019 08 23 21 59 49

その後「Start building」をクリックします。

2019 08 23 22 01 40

タスクが走り出します。

2019 08 23 22 02 49

環境変数を入れていないのでビルドが落ちます。

2019 08 23 22 04 41

プロジェクトの設定から「Environment Variables」を設定します。

2019 08 23 22 05 02

NameにUNITYLICENSE2019、Valueにさきほどbase64でエンコードしたデータを貼り付けます。

2019 08 23 22 07 56

もう一度CIを走らせます。 するとパスするのがわかります。

2019 08 23 22 11 15

試しにテストを落とすコミットをしてみます。 NewPlayModeTestScript.csに次のようにテストを追加してみます。

using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Tests {
    public class NewPlayModeTestScript {
        // A Test behaves as an ordinary method
        [Test]
        public void NewPlayModeTestScriptSimplePasses () {
            // Use the Assert class to test conditions
        }

        // A UnityTest behaves like a coroutine in Play Mode. In Edit Mode you can use
        // `yield return null;` to skip a frame.
        [UnityTest]
        public IEnumerator NewPlayModeTestScriptWithEnumeratorPasses () {
            // Use the Assert class to test conditions.
            // Use yield to skip a frame.
            yield return null;
        }

        [Test]
        public void OnePlusOneEqualToThree () {
            Assert.That (1 + 1, Is.EqualTo (3));
        }
    }
}

CIがFAILEDになるのが確認できました。

2019 08 23 22 15 38

2019 08 23 22 15 25

テストを修正します。

using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Tests {
    public class NewPlayModeTestScript {
        // A Test behaves as an ordinary method
        [Test]
        public void NewPlayModeTestScriptSimplePasses () {
            // Use the Assert class to test conditions
        }

        // A UnityTest behaves like a coroutine in Play Mode. In Edit Mode you can use
        // `yield return null;` to skip a frame.
        [UnityTest]
        public IEnumerator NewPlayModeTestScriptWithEnumeratorPasses () {
            // Use the Assert class to test conditions.
            // Use yield to skip a frame.
            yield return null;
        }

        [Test]
        public void OnePlusOneEqualToTwo () {
            Assert.That (1 + 1, Is.EqualTo (2));
        }
    }
}

きちんとコミットのたびテストの走ることが確認できます。

2019 08 23 22 19 27

おわりに

テストは書くだけではなくて定期的に実行される環境を作るのが重要だと思います。 CircleCIを利用することでコミット時にテストを走らせられるようになりました。

今回のソースコードのリポジトリ

参考リンク

次のリポジトリがとても役に立ちました。

今回利用させてもらったRuntimeUNitTestToolKit。