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

はじめに

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

  • Unity: 2019.1.14f1

テストを書く

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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で次のコマンドを実行します。

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

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

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

1
cat Unity_v2019.x.ulf | base64

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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をクリックします。

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

タスクが走り出します。

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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になるのが確認できました。

テストを修正します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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));
}
}
}

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

おわりに

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

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

参考リンク

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

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