yotiky Tech Blog

とあるエンジニアの備忘録

Unity Unit Test(単体テスト)入門 - VContainer

目次

はじめに

  • この一連の記事は
    • UnityのUnity Test Frameworkを使ったテストに関して調べたメモ書きに補足を足したもの
    • Unityのテスト、およびDIコンテナ、モックライブラリの基本的な使い方やそれぞれの役割など入門レベルの解説
    • ライブラリのリファレンス的な使い方などについては公式や他の記事参照
    • 実機テストやCIは含まない
    • TDDに関しては考慮しない

シリーズの目次

環境

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のようなものは用意されてなさそう

サンプル

アプリ側

  • Sword クラス
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.");
    }
}
  • Player クラス
public class Player
{
    private ISword _sword;
    public ISword Sword => _sword;

    public Player(ISword sword)
    {
        _sword = sword;
    }
}
  • VContainerSample クラス
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>();
    }
}

テストコード

  • Mockクラス
public class SwordMock : ISword, IInitializable
{
    private string _name;
    public string Name => _name;
    public SwordMock(string name)
    {
        this._name = name;
    }

    public void Initialize()
    {
        // ZenjectUnitTestFixtureでは呼ばれない
        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();

        // Injectされたいインスタンスを渡すこともできる
        _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;
    }
}

参考