目次
はじめに
- この一連の記事は
- 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
VContainer
手順
- VContainerを追加する
https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.13.2
- アプリケーションのasmdefに、
VContainer
の参照を追加する
- 基本的な使い方
- Inject対象を実装する
- インジェクト方法は4つ
- フィールドインジェクション(メンバにInject属性をつける)
- プロパティインジェクション
- メソッドインジェクション
- コンストラクタインジェクション(属性なし、引数にバインドされる)
- コードストリッピング対策でInject属性をつける場合がある
- MonoBehaviour
- コンストラクタ使えないので初期化メソッドを定義してメソッドインジェクション
MonoBehaviour
へのインジェクトは自動ではおこなれないので以下のどれかが必要
- Inspectorから
LifetimeScope
に対象のGameObjectを登録する
- コードから
RegisterComponent*
で登録する
Instantiate
する場面では、IObjectResolver.Instantiate
を代用する
LifetimeScope
を継承したコンポーネントで、依存関係を定義する
- C#スクリプトを作成する時に「~LifetimeScope」にするとテンプレが適用される
- Hierarchyで
LifetimeScope
を追加する
テストでの使い方
- テストのasmdefに、
VContainer
の参照を追加する
- Zenjectのように特殊なBaseクラスは用意されていないので、通常と同じように
TestFixture
属性をつけたクラスで実装する
ContainerBuilder
を使って依存関係を定義して、Build
してResolverを生成する
- インジェクトされたいインスタンスに
Inject
するか、Resolve
でインスタンスを取得して、それを使って検証する
- モックライブラリを使ってない場合は、テスト用のモッククラスを定義するなどする
- Zenjectのようにシーンを読み込む時にContextを上書き(先割り込み?)する
StaticContext
のようなものは用意されてなさそう
サンプル
アプリ側
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 VContainerSample : MonoBehaviour
{
[Inject]
private Player player;
public Player Player => player;
void Start()
{
Debug.Log(player.Sword);
}
}
- VContainerLifetimeScope クラス
public class VContainerLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<ISword, Sword>(Lifetime.Transient)
.WithParameter("material", "オリハルコン")
.WithParameter("author", "オルテガ");
builder.Register<Player>(Lifetime.Transient);
builder.RegisterComponentInHierarchy<VContainerSample>();
}
}
テストコード
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.");
}
}
[TestFixture]
public class VContainerTest
{
[Inject]
private ISword _target;
private IObjectResolver _container;
[SetUp]
public void CommonInstall()
{
var builder = new ContainerBuilder();
builder.Register<SwordMock>(Lifetime.Transient)
.WithParameter("本打ちの棍棒")
.AsImplementedInterfaces();
builder.Register<Player>(Lifetime.Transient);
builder.RegisterComponentOnNewGameObject<VContainerSample>(Lifetime.Transient);
_container = builder.Build();
_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<VContainerSample>();
Assert.That(target.Player.Sword.Name, Is.EqualTo("本打ちの棍棒"));
yield return null;
}
}
参考