Unity コンポーネントの単体テストでのフィールドの初期化
Unity コンポーネントの単体テストでのフィールドの初期化
Unity コンポーネントの単体テスト (NUnit などでのテスト) は正直めんどくさいです。いつも書きなれているピュアなクラスに対するテストと違い、Unity への深い理解が必要になります。
ゲームのロジックはピュアなクラスに書いて、Unity コンポーネントからそれを呼び出すスタイルをお勧めします。これなら、Unity でテストをどうするか? みたいなことに悩まされません。
とはいえ、Unity コンポーネントをテストする必要もあるわけでして。その時に面倒なのが、Unity コンポーネントのシリアライズ可能なフィールドの初期化です。
何とかなったので報告です。
面倒な理由
Unity コンポーネントは Unity Engine 管理しており、MonoBehaviour を継承しないピュアなクラスとライフサイクルが異なります。
例えば、ピュアなクラスは次のようにインスタンス化できます。
var target = new Sample("さんぷる");
// 以下 Sample クラスのテスト
Unity コンポーネントで、愚直に書くと、次のようになります。
var gameObject = new GameObject();
var target = gameObject.AddComponent<Sample>();
target.text = "さんぷる";
// 以下 Sample クラスのテスト
しかし、この方法には問題があります。
-
text
フィールドへの値の設定が、Awake()
やStart()
メソッドより後に実行される。これは通常の Unity の動作とは逆です。 -
text
フィールドのアクセス修飾子を公開する必要がある。(ないしは、値を書き換えるプロパティやメソッドの作成)
もちろん、がんばればこれらの問題は解決できます。しかし、そのためにはテストのために Unity コンポーネントを大きく修正する必要があります。テストのための修正が Unity コンポーネントの複雑さを招き、思いもよらぬバグを産みだします。テストコードを書いたはずなのに品質がかえって下がるかもしれません。
もっとシンプルに
テストは大事ですが、テストのために複雑化するのはあまりよくありません。というわけで、もっとシンプルな方法がないかを改めて考えます。
Unity Engine は GameObject が非アクティブな間 Awake()
メソッドを呼び出しません。これを使えば、フィールド変数の初期化後に Awake()
メソッドを呼び出すことができます。
class MyClass : MonoBehaviour
{
[SerializeField] private string text;
void Awake()
{
// 初期化
}
public static MyClass AttachTo(GameObject gameObject, string text)
{
var myClass = gameObject.AddComponent<MyClass>();
myClass.text = text;
return myClass;
}
}
テストコードでは次のようにします。
var gameObject = new GameObject();
gameObject.SetActive(false); // いったん非アクティブにしておく
var myClass = MyClass.AttachTo(gameObject, "なんか");
gameObject.SetActive(true); // アクティブにして Awake() を実行させる
無事、text
フィールドを非公開なままで、Awake()
メソッドより先にフィールドの初期化を完了させることができました。
テストのための Unity コンポーネントを修正する必要はありますが、AttachTo()
メソッド以外はテストのための修正はありません。また、AttachTo()
メソッドはテストに限らず、コードで Unity コンポーネントを組み立てるときにも使えます。
もうちょっとしっかり
GameObject を非アクティブするのをうっかり忘れそうなので、非アクティブかどうかを AttachTo()
メソッドで検査してもいいかもしれません。
もうちょい頑張るなら、AttachTo()
メソッドで GameObject を一時的に非アクティブにする方法もあります。
public static MyClass AttachTo(GameObject gameObject, string text)
{
var isActive = gameObject.activeSelf;
gameObject.SetActive(false);
try
{
var myClass = gameObject.AddComponent<MyClass>();
myClass.text = text;
return myClass;
}
finally
{
gameObject.SetActive(isActive);
}
}
毎回書くのは面倒ですけど、ヘルパークラスを作っておけばそうでもありません。
class DisabledScope : IDisposable
{
private readonly GameObject _gameObject;
private readonly bool _originalActive;
public DisabledScope(GameObject gameObject)
{
_gameObject = gameObject;
_originalActive = gameObject.activeSelf;
gameObject.SetActive(false);
}
public void Dispose()
{
_gameObject.SetActive(_originalActive);
}
}
public static MyClass AttachTo(GameObject gameObject, string text)
{
using var scope = new DisabledScope(gameObject);
var myClass = gameObject.AddComponent<MyClass>();
myClass.text = text;
return myClass;
}
最後に
Unity に限った話ではないのですが、特に古いフレームワークはテスタビリティが低く、大変です。
var gameObject = new GameObject();
var target = gameObject.AddComponent(new Sample("さんぷる"));
のように書けるだけで全然違うんですけどねぇ...
Discussion