yotiky Tech Blog

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

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

目次

はじめに

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

シリーズの目次

環境

NSubstitute

手順

  • NSubstituteを追加する
    • Extenject(Zenject)からの場合
      • AutoSubstitute.zipをインポートする
      • 同梱されているAutoSubstitute.zipを解凍する
      • AutoSubstituteフォルダごとTestFrameworkフォルダに配置する
      • Zenject-TestFramework.asmdefにNSubstitute.dllの参照を追加する
    • nugetからの場合

基本的な使い方

  • テストのasmdefに、NSubstitute.dllの参照を追加する
  • テストコードでSubstituteでモックを使ってテストする

  • インターフェース

public interface IShield
{
    string Name { get; }

    uint DefencePower { get; }

    void AddEffect(int value);

    uint CalcDefencePower();
}
  • テストコード
[TestFixture]
public class NSubstituteUnitTest
{
    [Test]
    public void MockObjectからのOutputの確認()
    {
        var mock = Substitute.For<IShield>();
        
        // プロパティのGet
        mock.Name.Returns("Name");
        // メソッドの戻り値
        mock.CalcDefencePower().Returns(100u);

        Assert.That(mock.Name, Is.EqualTo("Name"));
        Assert.That(mock.CalcDefencePower(), Is.EqualTo(100));
    }

    [Test]
    public void MockObjectへのInputの確認()
    {
        var mock = Substitute.For<IShield>();

        mock.AddEffect(-10);

        mock.ReceivedWithAnyArgs(1).AddEffect(-10);
    }

    [Test]
    public void Mockのコールバック()
    {
        LogAssert.Expect(LogType.Log, "AddEffect called.");

        var mock = Substitute.For<IShield>();

        mock.When(x => x.AddEffect(Arg.Any<int>()))
            .Do(_ => Debug.Log($"AddEffect called."));

        mock.AddEffect(0);

        mock.ReceivedWithAnyArgs(1).AddEffect(Arg.Any<int>());
    }
}

サンプル

アプリ側

public interface ISystemClock
{
    DateTime Now { get; }
}

public class Ticket
{
    private ISystemClock _clock;
    private uint _expireDays;

    public Ticket(ISystemClock clock, uint expireDays)
    {
        _clock = clock;
        _expireDays = expireDays;
    }

    public DateTime Issue()
    {
        return _clock.Now.AddDays(_expireDays);
    }
}

with Extenject(Zenject)

[TestFixture]
public class NSubstituteZenjectUnitTest : ZenjectUnitTestFixture
{
    [Test]
    public void SimpleTest()
    {
        var mock = Substitute.For<ISystemClock>();
        mock.Now.Returns(new DateTime(2023, 1, 1));

        Container.BindInstance(mock)
            .AsTransient();

        Container.Bind<Ticket>()
            .AsTransient()
            .WithArguments(10u);

        var target = Container.Resolve<Ticket>();
        Assert.That(target.Issue, Is.EqualTo(new DateTime(2023, 1, 1).AddDays(10)));
    }
}

with VContainer

[TestFixture]
public class NSubstituteVContainerTest
{
    [Test]
    public void SimpleTest()
    {
        var mock = Substitute.For<ISystemClock>();
        mock.Now.Returns(new DateTime(2023, 1, 1));

        var builder = new ContainerBuilder();

        builder.RegisterInstance(mock);

        builder.Register<Ticket>(Lifetime.Transient)
            .WithParameter(typeof(uint), 10u);

        var container = builder.Build();

        var target = container.Resolve<Ticket>();
        Assert.That(target.Issue, Is.EqualTo(new DateTime(2023, 1, 1).AddDays(10)));
    }
}

参考