本記事の執筆者はMina Pêcheuxです。
大規模なUnityプロジェクトに取り組めば取り組むほど、小規模なUnityプロジェクトに取り組んでいるときには考えもしなかったような、堅実で、長期的なプロジェクトを構築するための基本的な専門領域がいろいろあることがわかります。特に、ユニットテストは、複雑で少し退屈な場合もありますが、必須です。ユニットテストでは、コードベースを小さな論理ユニットに分割し、それぞれがきめ細かく動作することを確認することで、アプリの強固な基盤と、コードのリグレッション(機能後退)を回避することを保証できます。
ですが、ユニットテストを設定して自動的に実行させるのは、Unityではかなり厄介な場合があります。そこで今日は、Codemagicで構築したCI/CDパイプラインを使って、Unityでユニットテストを作成し自動化する方法についてご説明いたします。
そこで、本記事では以下について学べます:
- 効率的なアジャイル開発に自動ユニットテストが必要な理由。
- Unityでユニットテスト自動化を設定する際の問題点。
- サンプルプロジェクトとして使用する、基本的なPongゲームの作成方法。
- Pongゲームのユニットテストを設定する方法。
- Unityプロジェクトのユニットテストとビルドを自動化するためのCodemagicの設定方法。
それでは、まいりましょう!
DevOpsと自動化
“DevOps”とは、「開発」と「運用」(operations、“ops”)を連結させた言葉で、様々なことを表現できるふわっとした言葉です。例えば、チーム(DevOpsチーム)、一連の慣行や哲学など…。全体としては、開発とITの世界の最良のものを使って、より良いコードを生産し、*簡単かつ継続的にリリースを提供する**ということです。
DevOpsは、例えばウェブ開発者がよく従う哲学です。大きなオンラインプラットフォームで仕事をする場合、定期的に新しいバージョンを出荷し、顧客のためにソフトウェアをアップグレードする必要があります。オンラインサービスなので、基本的にはコンピューターのどこかにあるソースコード(どんなクラウドも他人のコンピューターに過ぎないことは周知の事実です)を最新のアップデートに置き換えて、新しいリリースを公開する必要があります。
ですが、大きなバグを公開したり、間違ったコード行のためにインフラがブロックされるのは当然避けたいものです。また、新しいものをリリースする頻度を考えると(それが大規模なリファクタリングであれ、非常に小さなバグ修正であれ)、バグに対するテストと公開のすべてを 自動的にできれば最高ですよね?
それが、DevOpsの核心的な関心事の1つであり本質です。つまり、継続的インテグレーションと継続的デリバリー、またはCI/CDです。これらは、従来のソフトウェアエンジニアリングにおける重要なプロセスで、バージョン管理ツールとともに製品のデリバリーを自動化するのと同時に、このデリバリーが何も問題を起こさず、ダウンタイムを生じさせず、迅速な修正を必要としないことを確認することを目的としています。
だからこそ、ユニットテストはDevOpsと自動化のための基本概念なのです。つまりこれは、リリースするコードが有効であり、リリース時に製品をクラッシュさせないことを保証するため、この自動化のチェーンにおける重要なブロックの1つとなっています。そのため、CI/CDプロセスでは通常、ユニットテストが必須ステップとしてどこかに組み込まれています。基本的に、ユニットテストが失敗した場合、デリバリープロセスは中断され、バグのあるバージョンはリリースされません。
Unityと自動化
Unityの必要最小限のCI/CDツールの問題点
ただ、個人的には、Unityはデリバリーの自動化にはあまり最適化されていないと思っています。このエンジンで作業する場合、DevOpsの思想をそれほど容易に実現できません。
公平を期すためにお伝えすると、このギャップを埋め、Unityプロジェクトを自動化する必要性を満たそうとするツール、すなわちUnityクラウドビルドが現在Unityエンジンに組み込まれています。しかし、それらには限界があります。特に:
- Unityクラウドビルドでは、公開ができません。多くの場合、CI/CDパイプラインは、ターゲットデバイスやオンラインストア(Google PlayやApp Storeなど)に実際にアップデートを配信することで終わります。Unityクラウドビルドではそれができないので、この最終ステップを手動で行う必要があります!
- また、このツールはかなり遅いです。キャッシュオプションがあったとしても、Unityクラウドビルドは現時点では明らかに速度が最適化されていません。
- また、Unityクラウドビルドは Gitとの相性があまり良くありません(UnityはPerforceに依存していることも理由の一つです。Perforceは通常、デジタル資産の共有には適していますが…あまり多くの開発者に使われていません!)。
- 最後に、Unityクラウドビルドは純粋なUnityプロジェクトに対応するため、Unityと、例えばReact Nativeや他のフレームワークを組み合わせたプロジェクトでは、Unityクラウドビルドを使用することはできません。
これらのことから、Unityクラウドビルドは素晴らしいものとなっていますが、UnityのCI/CDについてはまだ不完全な選択肢であると言えます。自動化はある程度までしか役に立ちません。また、かなり「エッジケース(極端な動作で発生する問題や状況)的」な習慣を強制されることも考えられます…。
Codemagicはどのように役に立つのか
一方、こうした様々な苦労をしたくないという方は、Unityクラウドビルドに代わるものとして、**Codemagic**が考えられます!
このオンラインツールは、Gitリポジトリを接続し、数個の設定手順を完了するだけで、CI/CDを迅速にセットアップできます。また、特定のハードウェアにアクセスするのに便利な方法です。Codemagicを使用すると、Mac、iOS、Windows、Androidなどに対応したゲームを簡単に構築できます。また、このサービスは価格の面でも拡張性があり、App StoreやGoogle Playで簡単にプロジェクトを公開できます。
そこで今日は、Codemagicを使って、PongのようなUnityゲームのサンプルに一部自動化を設定する方法をご紹介いたします。
基本例:Pongの再作成
注意:このサンプルプロジェクトのコードはGitHubで公開しています! 🚀
ここまで、UnityでCI/CDを実装する際の問題点と可能な解決策、さらに言えば、この自動化プロセスにおけるユニットテストの重要性について述べてきましたが、基本例としてUnityのサンプルプロジェクトを作成し、Codemagicを使って「自動化」してみましょう。では、有名なPongゲームを、ユニットテストとともに再作成します!
基本的なPongゲームのコーディング
このプロジェクトでは、まったく新しいゲームを発明して、ゲームデザインを議論することがポイントではありません。そうではなく、非常に有名なゲームであるPongを取り上げて、部分的に再コードすることです。そして、ユニットテストを使ってちゃんと動くかどうか確認します。
このゲームの主旨は、以下を持つことです:
- 各パドル(左パドルは
<W>
と<S>
、右パドルは上下矢印キー)を制御するPaddleManager
C# クラス。これらの動きは、カメラビューに拘束されます。 - パドルを作成し、スコアを記録し、ランダムな速度でボールをリスポーン(再登場)させる
GameHandler
とGameManager
。私はユニットテストでの実行を容易にするために、コア関数をGameHandler
クラス(MonoBehaviourではなく、バニラ C#クラス) に分離することにしました! - 画面の左右の境界線にある2Dトリガーのコライダーに適用される
ScoreTrigger
。これは、ボールがアウトオブバウンズになった(境界外に出た)かどうかをチェックし、アウトオブバウンズになった場合は、相手に1点与え、ボールをリスポーン(再登場)させます。
ゲームは一度始まると永遠に続くため、追加のメニューやポーズ機能は用意せずに、シンプルにこのPongゲームのコアな仕組みに集中していきます。様々なバウンドや衝突をUnityの2D物理演算に頼り、以下の要素でシーンを用意することにします:
- 2つのパドル(BoxCollider2Dコンポーネントと
PaddleManager
スクリプトを使った2Dの長方形)。 - ボール(CircleCollider2DとRigidbody2Dコンポーネントを持つ2Dの円)。
- 画面の上下にある2つの物理的な壁(カメラビューにボールを拘束し、画面の境界でボールを「跳ね返す」ようにするため)。
- 画面の左側と右側にある2つのトリガー(それぞれ
isTrigger
フラグが有効でScoreTrigger
スクリプトが添付されたBoxCollider2Dコンポーネントを持つ、目に見えない2D要素)。
Unityのテストフレームワークパッケージのインストール
Unityでユニットテストを行う場合、公式のUnityテストフレームワークパッケージで提供されているユーザーフレンドリーなツールを利用できます。これはUnityチームによって作成されたモジュールで、C#のユニットテストを簡単に書くためのイケてるデバッグインターフェイスとコードのショートハンド(省略記法)を提供します。これをプロジェクトに追加するのは非常に簡単です。Unityのパッケージマネージャーウィンドウを開き、テストフレームワークモジュールを検索し、「インストール」をクリックします(あるいは、すでにインストールされているかどうかを単に確認するだけです)。
プロジェクトに追加されたら、Window
の下にTest Runner
という新しいオプションが利用可能になるのがお分かりになると思います。
Unityでのテストアセンブリとテストスクリプトを作成
このウィンドウでは、テストを一緒にグループ化し、正しい依存関係を取得するための新しいアセンブリを簡単に作成できます。アセンブリ定義資産を作成したら、「テストスクリプトの作成(Create Test Script)」ボタンをクリックして、テストケースの例をいくつか含む新しいC#テストスクリプトの追加もできます。
私の場合、すべてのテストを3つのテストクラス、つまり、BallTests
、PaddleTests
、ScoreTests
で書くことにします。各クラスには、さまざまな機能を確認するための「粒度」の異なるテストケースが用意されます。
C#のメソッドをテストセッションに登録するには、Test
メソッド属性(通常の関数の場合)かUnityTest
メソッド属性(コルーチンの場合)を与えるだけで良いです。このメソッドには、実際にテストをいくつか実行するための一つ以上のアサーション (Assert
クラスを使用) が含まれていなければなりません。
一つのテストケースにおける粒度(すなわちアサーション数)はあなた次第ですが、一般的には、各テストケースのスコープは、可能な限り制限する必要があります。例えば、私のBallTests
クラスでは、 GameHandler.InitializeBall()
関数を使用したときにボールが適切にリスポーン(再登場)することを確認するテストケースを1つだけ作成します:
using NUnit.Framework;
using UnityEngine;
using Pong; // my game scripts namespace
namespace EditorTests
{
public class BallTests
{
private GameHandler _handler = new GameHandler();
[Test]
public void ShouldInitializeBall()
{
GameObject ballObj = new GameObject();
Rigidbody2D ball = ballObj.AddComponent<Rigidbody2D>();
_handler.InitializeBall(ball);
Assert.AreEqual(ball.transform.position, Vector3.zero);
Assert.AreNotEqual(ball.velocity, Vector2.zero);
}
}
}
このテストは、ボールがリセットされた後、2つのことが真であることを主張しています。1つ目は、ボールが画面の中央にあること、2つ目は、ボールが非ヌル速度を持っていることです。
ScoreTests
クラスは、左側と右側のプレイヤーの得点を区別します。それぞれの状況は、その独自のテストケースで処理されます。
using NUnit.Framework;
using Pong;
namespace EditorTests
{
public class ScoreTests
{
private GameHandler _handler = new GameHandler();
[Test]
public void ShouldScoreLeft()
{
_handler.scoreLeft = 0;
_handler.scoreRight = 0;
_handler.ScorePoint(true);
Assert.AreEqual(_handler.scoreLeft, 1);
Assert.AreEqual(_handler.scoreRight, 0);
}
[Test]
public void ShouldScoreRight()
{
_handler.scoreLeft = 0;
_handler.scoreRight = 0;
_handler.ScorePoint(false);
Assert.AreEqual(_handler.scoreLeft, 0);
Assert.AreEqual(_handler.scoreRight, 1);
}
}
}
一方で、本当に面白いのがPaddleTests
クラスです!
最初の複数のテストケースは、ゲームの初期化に関するものです。私は、パドルが正しくインスタンス化され、配置され、設定されているかどうかをチェックします。ですが、次のメソッドはコルーチン(UnityTest
属性を使用)で、私がパドルを動かしている間のごく一部の時間をシミュレートしています。
2つのパドルの動作は全く同じで、入力だけが違うので、左右の区別をせず、片方だけをテストできるとしています。左のパドルで4つのテストを実行します:
ShouldMovePaddleUp()
とShouldMovePaddleDown()
は、単にMoveUp()
とMoveDown()
関数がパドルの位置を変更することをアサートしています。- そして、
ShouldKeepPaddleBelowTop()
とShouldKeepPaddleAboveBottom()
は、この動きがカメラの境界に制約され、上端と下端をオーバーシュート(超過)しないことをアサートします。
アサーションを除けば、これらのメソッドはすべて同じロジックに従います。まず、timeScale
を大きくして、テストがより速く実行できるようにします (これは基本的に、ビデオゲームをクイックモードで実行するようなものです)。次に、私が望む状況をシミュレートします。そして最後に、timeScale
を通常の値である1にリセットします。
一例として、最初のテストケースであるShouldMovePaddleUp()をご紹介いたします:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Pong;
namespace EditorTests
{
public class PaddleTests
{
private GameHandler _handler = new GameHandler();
// ...
[UnityTest]
public IEnumerator ShouldMovePaddleUp()
{
// increase timeScale to execute the test quickly
Time.timeScale = 20f;
(GameObject left, _) = _handler.CreatePaddles();
PaddleManager pm = left.GetComponent<PaddleManager>();
// move paddle up
float startY = left.transform.position.y;
float time = 0f;
while (time < 1)
{
pm.MoveUp();
time += Time.fixedDeltaTime;
yield return null;
}
Assert.Greater(left.transform.position.y, startY);
// reset timeScale
Time.timeScale = 1f;
}
// ...
}
他の関数を確認したい場合は、GitHubリポジトリ 🚀をご覧ください。
コンソールからUnityを起動する
まず、Unityのユーザーインターフェイスを使って、プログラムで、かつ自動化されたワークフロー内でチェックを実行することは、明らかにできません。その代わりに、テストを実行し、ユニットテスト段階をCI/CDパイプラインに統合するのに、Unityのコマンドライン実行に依存する必要があります。
実行するユニットテストと終了コードをよりよく制御するために、独自のテスト実行関数を書いて、コマンドラインから呼び出すことにします。
では、テストスクリプトの横に新しいRunner
C#クラスを次のコードで作成してみましょう:
using UnityEditor;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
public static class Runner
{
private static TestRunnerApi _runner = null;
private class MyCallbacks : ICallbacks
{
public void RunStarted(ITestAdaptor testsToRun)
{}
public void RunFinished(ITestResultAdaptor result)
{
_runner.UnregisterCallbacks(this);
if (result.ResultState != "Passed")
{
Debug.Log("Tests failed :(");
if (Application.isBatchMode)
EditorApplication.Exit(1);
}
else
{
Debug.Log("Tests passed :)");
if (Application.isBatchMode)
EditorApplication.Exit(0);
}
}
public void TestStarted(ITestAdaptor test)
{}
public void TestFinished(ITestResultAdaptor result)
{}
}
public static void RunUnitTests()
{
_runner = ScriptableObject.CreateInstance<TestRunnerApi>();
Filter filter = new Filter()
{
testMode = TestMode.EditMode
};
_runner.RegisterCallbacks(new MyCallbacks());
_runner.Execute(new ExecutionSettings(filter));
}
}
これは、Unityテストフレームワークを使用して、プログラムによるテストの実行と、すべてのテストが終了したときに特定のコールバックルーチンを実行するものです。
ここで、コマンドラインからUnityの実行ファイルを呼び出すことで、ターミナルで実行できます。パスはWindowsとMacで少し違います (https://docs.unity3d.com/Manual/EditorCommandLineArguments.html)。以下は、OS Xシステムの例です:
/Applications/Unity/Hub/Editor/2020.3.20f1/Unity.app -batchmode -projectPath /path/to/your/project -nographics -executeMethod Runner.RunUnitTests -logFile unit_test_logs.log
コマンドラインの最初の部分は、Unityの実行ファイルへのパスです。batchmode
と-nographics
は、ユーザーインターフェイスなしでコンソールモードでエディタを実行するためのものです。executeMethod
オプションは私たちの関数を直接実行し、-logFile
オプションは実行ログの出力ファイルへのパスを指定します。
projectPath
オプションは、あなたご自身のUnityプロジェクトのパスに合わせることをお忘れにならないでください!
Codemagic CI/CDのセットアップ
最後のステップは、このユニットテストステップをCodemagicのワークフローに統合し、自動化されたCI/CDプロセスの一部とすることです。
**重要なお知らせ:**本記事の公開時では、Unityのビルドワークフローでは特別なCodemagicアカウントが必要で、取得するにはCodemagicに連絡する必要があります。そうしないと、UnityがインストールされたMac ProやWindowsのインスタンスにアクセスできないため、このCI/CDチュートリアルはうまくいきません! また、Pro Unityライセンス が必要です。というのも、ビルドと公開を正しく動作させるために、ワークフロー中にライセンスを有効にする必要があります。
Codemagicは、アプリに依存しています。すべてのプロジェクトは、リモートリポジトリ(GitHub、GitLab、Bitbucket、またはカスタム)からコードをプルするアプリです。Codemagicのアカウントを作成したら、ダッシュボードに移動して新しいアプリの作成と設定ができます。
まず、プロジェクトのコードをプルしたいGitリポジトリをリンクする必要があります:
次に、アプリをもう少し設定できます。これは特に、環境変数をいくつか追加して行います。これは、認証情報などの機密データをGitリポジトリにコミットせずに追加する安全な方法です!
Unityアプリを構築するためのCodemagicドキュメント(Codemagic docs for building Unity apps)で説明されているように、3つの環境変数、UNITY_SERIAL
、UNITY_USERNAME
、UNITY_PASSWORD
を定義する必要があります。これらはすべて、必ずunityグループに追加してください。
次のステップは、ビルド設定パネルのビルドを手動でクリックする代わりに、プログラムを使用してプロジェクトをビルドするために、Editor/Build.cs
としてC#ビルドスクリプトをプロジェクトに追加します。
using System.Linq;
using UnityEditor;
using UnityEngine;
public static class BuildScript
{
[MenuItem("Build/Build Mac")]
public static void BuildMac()
{
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
buildPlayerOptions.locationPathName = "mac/" + Application.productName + ".app";
buildPlayerOptions.target = BuildTarget.StandaloneOSX;
buildPlayerOptions.options = BuildOptions.None;
buildPlayerOptions.scenes = GetScenes();
Debug.Log("Building StandaloneOSX");
BuildPipeline.BuildPlayer(buildPlayerOptions);
Debug.Log("Built StandaloneOSX");
}
private static string[] GetScenes()
{
return (from scene in EditorBuildSettings.scenes where scene.enabled select scene.path).ToArray();
}
}
ここでは、Mac用のビルドしか定義していませんが、Codemagicドキュメント では、他のプラットフォームのビルド設定がいくつか紹介されています。
Codemagicの本当に良いところは、ワークフロー定義ファイルで実際に何でもできることです。この設定は、プロジェクトのルート (Assets/
メインフォルダの隣) に置いてある**codemagic.yaml
ファイル**で定義します! このワークフローは非常にシンプルなため、単に、CI/CDプロセスのさまざまなステップを実行するシェルコマンドのリストを、以下のようにつなげるだけです:
workflows:
unity-mac-workflow:
# Building Unity on macOS requires a special instance type that is available upon request
name: Unity Mac Workflow
environment:
groups:
# Add the group environment variables in the Codemagic UI (either in Application/Team variables) - https://docs.codemagic.io/variables/environment-variable-groups/
- unity # <-- (Includes UNITY_SERIAL, UNITY_USERNAME, UNITY_PASSWORD)
vars:
UNITY_BIN: /Applications/Unity/Hub/Editor/2020.3.20f1/Unity.app/Contents/MacOS/Unity
scripts:
- name: Activate License
script: $UNITY_BIN -batchmode -quit -logFile -serial ${UNITY_SERIAL?} -username ${UNITY_USERNAME?} -password ${UNITY_PASSWORD?}
- name: Run Unit Tests
script: $UNITY_BIN -batchmode -executeMethod Runner.RunUnitTests -logFile -nographics -projectPath .
- name: Build
script: $UNITY_BIN -batchmode -quit -logFile -projectPath . -executeMethod BuildScript.BuildMac -nographics
artifacts:
- "mac/UnitTestingPong.app"
publishing:
scripts:
- name: Deactivate License
script: $UNITY_BIN -batchmode -quit -returnlicense -nographics```
ご覧の通り、このファイルでは、実行ステップのほか、ワークフローを実行するインスタンスの種類、読み込む環境変数、生成する成果物などを指定しています。
environment
の部分は、以前に “unity”グループで定義したすべてのenv varsと、その場でカスタムしたいくつかのvars (UNITY_BIN
など)を“inject”できるようにします。scripts
ブロックは、ワークフローの様々なステップを定義します。
- まず、Pro Unity Licenseをアクティベートし、必要なツールすべてにアクセスできるようにします。
- そして、ユニットテストを実行します。すべてが合格なら(つまり、終了コードが0なら)、次のステップに進みます。そうでない場合、ワークフローは直ちに中断され、何も公開されません。
- 次に、実際に新しいバージョンをアプリにビルドし、ビルドの成果物として保存します。
- そして最後に、ライセンスを解除し、クリーンアップをいくつか実行します。
クリーンアップの前に、こちらで解説されているように、ストアに公開するステップを追加することも可能です。
最終的には、1つの成果物、つまり、実行可能なPongゲームである実行ファイルそのものを作成することになります (artifacts
ブロックをご参照ください)。
これらをすべてコミットしてリポジトリにプッシュしたら、Codemagicのダッシュボードに戻り、リストからこのワークフローを選んでアプリのビルドを開始するだけです。
右側にCI/CDの各ステップが表示されます。まず、リモートマシンが初期化され、次にコードがあなたのリポジトリから取り出され、設定ファイルcodemagic.yaml
にあるステップが実行されます。
何らかの理由でワークフローが1の終了コードで失敗した場合(例えば、ユニットテストに失敗した場合)、処理が中断されます:
そうでなければ、全体が最後まで実行され、アプリが自動的にデプロイされます。
ご自分のBuildsにアクセスして、ワークフローのビルドの様々な状態を確認できます。ここでは、各ビルドが使用したリポジトリとコミット、完了までにかかった時間が表示され、ワークフローで作成された成果物をダウンロードするためのリンクが表示されます。
結論
ジャジャーン! Codemagicのおかげで、UnityのCI/CDプロセスをユニットテストによって自動化し、コードが問題なく、リグレッション(機能後退)やバグが発生しないことを検証することができるようになりました! さらに、テストレポートやワークフロースケジューリングを利用することもできるようになりました。
このチュートリアルをお楽しみいただけましたら幸いです。また、もちろん、私にUnityチュートリアルを作ってほしい他のDevOpsトピックについてのアイデアがございましたら、お気軽にお伝えください。CodemagicのSlack、Medium、Twitterで私を見つけることができます。
このチュートリアルのサンプルプロジェクトとcodemagic.yaml
ファイルは、GitHubで見ることができます(こちら )。 🚀
Mina Pêcheuxさんは、フリーランスのフルスタックウェブ&ゲーム開発者です。また、コンピューターグラフィックス、音楽、データサイエンスなどにも情熱を注いでいます! 自らブログも運営されています。あるいは、TwitterでMinaさんを見つけることもできます。