レガシーコードとはテストのないコードのことである
テスト駆動開発 や 継続的インテグレーション/デリバリー といった言葉が注目される昨今ですが、その中心にあるのが言うまでもなく テスト です。
テストを蔑ろにしていた(画面をポチポチして動作確認していただけの)過去の自分を反省し、この潮流に乗るためにUnityでのテスト導入方法をまとめました。バージョンは 2018.3.2 です。説明はMacでの画面ベースになります。
Unityにおけるテストの導入
Test Runner
Test Runner と呼ばれる機能がUnityにはあります。私が使ってなかっただけで昔からあったんです。
しかし、起動は[Window]>[General]>[Test Runner]からと回りくどさも否めません。
2019.1+
@pCYSl5EDgo さんからの情報提供にある通り、バージョン2019.1以上を使っている人はShortcutManagerでTest Runnerの起動にキーを割り当てられるようになりました。
自分は⇧+⌘+Tが空いてたので、それに割り振りました。
2019.2+
バージョン2019.2以降ではテストフレームワーク用のpackageに分離されたようなので、Unity Package Managerからインストールしてください。
About Unity Test Framework
とりあえず作る
プロジェクトのAssets/で右クリックから[Create]>[Testing]を選びます。
[C# Test Script]がグレーアウトされています。ここは一旦[Tests Assembly Folder]を選択し、Testsというディレクトリを作成します。一緒にTests.asmdef
というファイルも作成されますが一旦無視します。
Assets/Tests/に移動し、もう一度右クリックから[Create]>[Testing]を選ぶと[C# Test Script]が選択できるようになっているので、NewTestScript.cs
を作ります。
するとTest Runnerの[PlayMode]タブに以下のようにデフォルトのテストが追加されます。
注意: Unity2018-TestRunner
というのは今回作ったプロジェクト名です。機能とは無関係です。
[Run All]が押せるようになっているので押してみます。
すると普段通りUnityEditorから実行されたような挙動を見せ、全テストに ✅ がついて終了します。
デフォルトのままでテストに何も書いてないので、全てパスします。
テストの目標は全て✅になり、❌が0になることです。
EditMode
とりあえずで[PlayMode]を試しましたが、もう一方の[EditMode]とはなんでしょうか。
https://docs.unity3d.com/ja/2018.1/Manual/testing-editortestsrunner.html
はっきりと違いが書かれた記述は見当たりませんが、
- EditMode: 起動が早くカジュアルに実行できる。Classの単体テストに使う。
-
PlayMode: シーンの実行とほぼ同等のため起動がやや遅い。シーンの動作確認や
Monobehaviour
を組み合わせた結合テストに使う。
ような認識でいます。
EditModeのテストコードはUnityの特殊ディレクトリであるEditor/配下に置く必要があるようです。
Tests/にEditorディレクトリを作り、そこで[C# Test Scrit]からNewTestScriptEditor.cs
を作ります。すると[EditMode]にNewTestScriptEditor
が追加され........
ません!
[PlayMode]のテストの一部とみなされています。
Editor/配下のテストコードが勝手に[EditMode]に振り分けられるのではないのです。
Assembly Definition Files
さっき無視したTests.asmdef
が鍵を握っています。
Unity2017.3以降から Assembly Definition Files いう機能が追加され、これがテストコードを作成する際の手順に大きく関わっているようです。
最初に行った[Create]>[Tests Assembly Folder]とは作成したディレクトリ配下にテストコード用のAssemblyを定義することに他なりません。
Tests Assembly Folder配下にTests Assembly Folderをさらに作ることはできないので、テストコードのディレクトリはEditMode用とPlayMode用とで分ける必要があるようです。
こちらの記事 (Unityでちゃんとテストを書きたい人のためのまとめ) の ここ を参考に、プロジェクトを1から作り直します。
- Assets/にTestsディレクトリを 普通に 作ります。
- Assets/Tests/でPlayModeディレクトリを[Tests Assembly Folder]から作成します。
- Assets/Tests/PlayMode/で[C# Test Script]を作成し、名前を
NewTestScriptPlay.cs
とします。 - Assets/Tests/にEditModeディレクトリを[Tests Assembly Folder]から作成します。
- Assets/Tests/EditMode/にある
EditMode.asmdef
を選択し、右のinspectorから Platforms で AnyPlatform にチェックが入っているのを、Editorだけにチェックが入った状態にして[Apply]を押します。 - Assets/Tests/EditMode/にEditorディレクトリを作成します。
- Assets/Tests/EditMode/Editor/で[C# Test Script]を作成し、名前を
NewTestScriptEdit.cs
とします。
これでやっとNewTestScriptEdit
がTest Runnerの[EditMode]タブに追加されました。
EditModeのテストは[Run All]してもサックと終わります。
つまり、EditorのPlatformを対象としたTest用 .asmdef
ファイルを持つディレクトリ下のEditor/下にあるTest ScriptがEditModeで実行できるのです。
追記:EditorのPlatformを対象としたTest用 .asmdef
ファイルを持つディレクトリ下であれば、Editor/ディレクトリは必須では無いようです(コメントのご指摘より)
ちなみに、Test Runnerのウィンドウから[Create EditMode Test Assembly Folder]をすればPlatforms=Editorの.asmdef
が作成されます。
最終的な配置
Assets/
└ Tests/
┝ EditMode/
│ ┝ Editor/
│ │ └ NewTestScriptEdit.cs
│ └ EditMode.asmdef
└ PlayMode/
┝ NewTestScriptPlay.cs
└ PlayMode.asmdef
いちいちAssemblyなんて気にするのは煩わしい気もしますが、テストコードがプロダクトコードに含まれてしまいアプリサイズが肥大化するのを防いでくれる面もあります。
テストの作成
ここまでで導入です。ここから実際にテストを書いていきます。
EditModeのテスト
テストの対象となるScriptを作成します。
public class MyClass {
private string name;
public MyClass() {
name = string.Empty;
}
public MyClass(string name) {
this.name = name;
}
public void SetName(string name) { this.name = name; }
public string GetName() { return name; }
}
name
というフィールドと2種類のコンストラクタ、name
にアクセスするためのSet/Getメソッドを持つごく簡単なクラスです。
EditModeでテストをするため、NewTestScriptEdit.cs
にテストコードを書きます
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Tests {
public class NewTestScriptEdit {
[Test]
public void MyClassTest1() {
var x = new MyClass();
Assert.IsEmpty(x.GetName());
x.SetName("Alice");
Assert.AreEqual("Alice", x.GetName());
}
[Test]
public void MyClassTest2() {
var x = new MyClass("Bob");
Assert.AreEqual("Bob", x.GetName());
}
}
}
Assert.XXX
の各評価式でテスト成否を判定します。
しかしコンパイラが 'MyClass' could not be found
といってエラーを吐き、参照できない様です。
前の手順で各テストのAssemblyを定義したので、今のままではMyClass
とNewTestScriptEdit
は別Assemblyの住人になっています。MyClass
を含んだAssemblyを定義し、EditTestのAssembly参照に追加して橋渡しをする必要があります。
- Assets/にて右クリックから[Create]>[Assembly Definition]で
MyProject.asmdef
を作成します。 - Assets/Tests/EditMode/へ移動し、
EditMode.asmdef
を選択します。 - 右のinspectorの Assembly Definition References の[+]を押し、リストに
MyProject.asmdef
を追加し、[Apply]します。
これにて、NewTestScriptEdit
からMyClass
が参照できるようになり、コンパイルとテスト実行ができるようになりました。
このAssembly参照の追加はPlayModeのNewTestScriptPlay
でも同様に必要です。
余談
例なので簡単なテストということもありますが、どんなに簡単なテストでもあると無いでは天地の差です。 冒頭の定義ならテストが無ければ問答無用でレガシーコード認定です。
また、簡単でも1度テストを書けば、以降の開発でもテストを書かなきゃという意識を自分にもメンバーにも植え付けることが出来ます。
PlayModeのテスト
PlayModeでのテスト実行はシーンを動かしてこそだと思ったので、簡単なプロジェクトを作成しました。
naninunenoy/unity-2018-TestRunner
矢印のキーに合わせて白のCube(以降は 豆腐 と呼ぶ)が移動するシーンです。また、豆腐が画面外に出ないように抑制する仕様を持っています。
Test RunnerのPlayModeで実行するTest Scriptです
Assets/Tests/PlayMode/SampleSceneTest.cs
移動判定をInterfaceで分離し、キー入力をテストで模擬するテクニックを使っています。
Unity EditorでSceneの挙動を見ながらTest RunnerのPlayModeのテストを実行することができます。
実機での確認
テストウィンドウから[Run all in player]を実行するとこで、iPhoneなどの実機でPlayModeテストを実行できるようです。
実機で動作を確認する
テストを導入するメリット
結構な手順があり、プロジェクトに.asmdef
を入れるとAssemblyを意識した開発を強いられるので導入するをのためらう人もいるかもしれませんが、その手間を補って余りあるメリットがあるとも考えています。
安心感をもてる
機械的なテストを網羅的に一通り流すことで、想定通りの実装ができているという自信になります。また、バグ修正によるデグレを検出でき、事前に防ぐことが出来るかもしれません。
しかしながら、どんなにテストを行おうともバグが無いことの証明にはなりません。白いカラスが存在しないことを証明するのがほぼ不可能なのと一緒です。
リファクタのしやすさ
汚いコード(自分が気に入らないコードとも言う)はリファクタリングしたくなりますが、動いているコードを変更することはデグレのリスクが伴います。テストが通ることを担保しながらリファクタリングができればその心配がなくなります。事前にテストを作っておけば自分や後任の人がコードをリファクタリングする助けになります。
テストを意識した設計
通常のシーケンスしか意識しないプログラムは要求さえ満たせばいいやという意識になり、汎用性に欠けがちになります。自分の実装するクラスや機能がテストからも呼び出されることを考慮する必要に迫られれば、DIなど新しい設計思想を取り入れるきっかけにもなります。
PlayModeの例で作ったプロジェクトでの豆腐を動かす処理を完全にInput.KeyDown
に依存させてしまうと該当シーンのテストが困難になります。しかし、この処理をInterfaceに抽象化することで、テストで自由に移動を行うことを可能にしています。また、この設計ならInput.KeyDown
からゲームパッドで操作するように仕様追加が起こった際にも改修がスムーズにいくかもしれません。
おわりに
ソフトウェアのテストに関しては捉え方が人それぞれだと思います。
バグはコードレビュー+人力による確認で潰すという方針もあるでしょう。または、アプリの仕様上自動テストが厳しい箇所があるのかもしれませんし、会社(プロジェクト)の文化も関わってくるでしょう。
しかし、Unityという開発環境を利用しているのであれば、Test Runnerをいう仕組みを利用しない手はないと考えます。まずはごく簡単なテストから初めてみてはどうでしょうか💡