目次
はじめに
- この一連の記事は
- UnityのUnity Test Frameworkを使ったテストに関して調べたメモ書きに補足を足したもの
- Unityのテスト、およびDIコンテナ、モックライブラリの基本的な使い方やそれぞれの役割など入門レベルの解説
- ライブラリのリファレンス的な使い方などについては公式や他の記事参照
- 実機テストやCIは含まない
- TDDに関しては考慮しない
シリーズの目次
環境
- Unity 2021.3.29f1
- Package
- Test Framework 1.3.8
- com.unity.test-framework / 1.3.8
- Extenject 9.2.0
- VContainer 1.13.2
https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.13.2
- Moq
- 4.7.99 (Extenject内蔵)
- 4.20.69 (NuGet)
- NSubstitute
- v2.0.3.0 for .Net v4.5 (Extenject内蔵)
- 5.0.0 (NuGet)
https://github.com/neuecc/UniRx.git?path=Assets/Plugins/UniRx/Scripts
https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
Extenject(Zenject)
手順
- AssetStoreからExtenjectを追加する
- アプリケーションのasmdefに、
zenject
の参照を追加する
- 基本的な使い方
- Inject対象を実装する
- インジェクト方法は4つ
- フィールドインジェクション(メンバにInject属性をつける)
- プロパティインジェクション
- メソッドインジェクション
- コンストラクタインジェクション(属性なし、引数にバインドされる)
- MonoBehaviour
- コンストラクタ使えないので初期化メソッドを定義してメソッドインジェクション
MonoInstaller
を継承したInstallerを作成して、依存関係を定義する
- HierarchyでContextを追加してInstallerをアサインする
テストでの使い方
- テストのasmdefに、
zenject
、Zenject-TestFramework
、Zenject-usage.dll
の参照を追加する
- テストタイプは3種類
- UnitTest
- IntegrationTest
- SceneTest
- UnitTestは、
ZenjectUnitTestFixture
を継承する
- IntegrationTestは、
ZenjectIntegrationTestFixture
を継承する
- UnityTest属性向け、フレームを跨ぐテスト
- Test属性使ってもコケたりはしない
- PlayModeでのみ使える
IInitializable
等のZenjectのインターフェースを使った機能も動作する
- SceneTestは、
SceneTestFixture
を継承する
- 継承元の
Container
プロパティを使って、テスト用の依存関係を定義する
- インジェクトされたいインスタンスに
Inject
するか、Resolve
でインスタンスを取得して検証する
- モックライブラリを使ってない場合は、テスト用のモッククラスを定義するなどする
- Zenject/Documentation/WritingAutomatedTests.md at master · modesttree/Zenject
サンプル
アプリ側
public interface ISword
{
string Name { get; }
}
public partial class Sword : ISword, IInitializable
{
private string _material;
private string _author;
public string Name => $"{_material}の剣";
public Sword(string material, string author)
{
_material = material;
_author = author;
Debug.Log($"{Name} made by {_author}");
}
public void Initialize()
{
Debug.Log("Initialize called.");
}
}
public class Player
{
private ISword _sword;
public ISword Sword => _sword;
public Player(ISword sword)
{
_sword = sword;
}
}
public class ZenjectSample : MonoBehaviour
{
[Inject]
private Player player;
public Player Player => player;
void Start()
{
Debug.Log(player.Sword);
}
}
public class ZenjectSampleInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.BindInterfacesTo<Sword>()
.AsTransient()
.WithArguments("鋼", "馴染みの鍛冶職人")
.IfNotBound();
Container.Bind<Player>()
.AsTransient()
.IfNotBound();
}
}
テストコード
public class SwordMock : ISword, IInitializable
{
private string _name;
public string Name => _name;
public SwordMock(string name)
{
this._name = name;
}
public void Initialize()
{
Debug.Log("Initialize called.");
}
}
UnitTest
[TestFixture]
public class ZenjectUnitTest : ZenjectUnitTestFixture
{
[Inject]
private ISword _target;
[SetUp]
public void CommonInstall()
{
Container.BindInterfacesTo<SwordMock>()
.AsTransient()
.WithArguments("試し打ちの棍棒");
Container.Bind<Player>()
.AsTransient();
Container.Bind<ZenjectSample>()
.FromNewComponentOnNewGameObject()
.AsTransient();
Container.Inject(this);
}
[Test]
public void InjectTypeTest()
{
Assert.That(_target, Is.InstanceOf<SwordMock>());
}
[Test]
public void InjectValueTest()
{
var target = Container.Resolve<ISword>();
Assert.That(target.Name, Is.EqualTo("試し打ちの棍棒"));
}
[UnityTest]
public IEnumerator UnityTest属性のTest()
{
yield return null;
var target = Container.Resolve<ZenjectSample>();
Assert.That(target.Player.Sword.Name, Is.EqualTo("試し打ちの棍棒"));
yield return null;
}
}
IntegrationTest
public class ZenjectIntegrationTest : ZenjectIntegrationTestFixture
{
[Inject]
private ISword _target;
private void CommonInstall()
{
PreInstall();
Container.BindInterfacesTo<SwordMock>()
.AsTransient()
.WithArguments("ひのきの棒");
Container.Bind<Player>()
.AsTransient();
Container.Bind<ZenjectSample>()
.FromNewComponentOnNewGameObject()
.AsTransient();
PostInstall();
}
[Test]
public void InjectTypeTest()
{
CommonInstall();
Assert.That(_target, Is.InstanceOf<SwordMock>());
}
[Test]
public void InjectValueTest()
{
CommonInstall();
var target = Container.Resolve<ISword>();
Assert.That(target.Name, Is.EqualTo("ひのきの棒"));
}
[UnityTest]
public IEnumerator UnityTest属性のTest()
{
CommonInstall();
yield return null;
var target = Container.Resolve<ZenjectSample>();
Assert.That(target.Player.Sword.Name, Is.EqualTo("ひのきの棒"));
yield return null;
}
}
SceneTest
public class ZenjectSceneTest : SceneTestFixture
{
[UnityTest]
public IEnumerator TestScene()
{
StaticContext.Container
.BindInterfacesTo<SwordMock>()
.AsTransient()
.WithArguments("妖刀村正");
StaticContext.Container
.Bind<Player>()
.AsTransient();
yield return LoadScene("ZenjectSample");
var injected = SceneContainer.Resolve<ISword>();
Assert.That(injected, Is.InstanceOf<SwordMock>());
yield return null;
var obj = GameObject.Find("GameObject");
var target = obj.GetComponent<ZenjectSample>();
Assert.That(target.Player.Sword, Is.InstanceOf<SwordMock>());
Assert.That(target.Player.Sword.Name, Is.EqualTo("妖刀村正"));
}
}
参考