目次
はじめに
- この一連の記事は
- UnityのUnity Test Frameworkを使ったテストに関して調べたメモ書きに補足を足したもの
- Unityのテスト、およびDIコンテナ、モックライブラリの基本的な使い方やそれぞれの役割など入門レベルの解説
- ライブラリのリファレンス的な使い方などについては公式や他の記事参照
- 実機テストやCIは含まない
- TDDに関しては考慮しない
シリーズの目次
- Unity Unit Test(単体テスト)入門
- Unity Unit Test(単体テスト)入門 - Unity Test Framework
- Unity Unit Test(単体テスト)入門 - Extenject(Zenject)
- Unity Unit Test(単体テスト)入門 - VContainer
- Unity Unit Test(単体テスト)入門 - Moq
- Unity Unit Test(単体テスト)入門 - NSubstitute(この記事)
- Unity Unit Test(単体テスト)入門 - 番外編 UniRx
環境
- 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
- Test Framework 1.3.8
NSubstitute
手順
- NSubstituteを追加する
- Extenject(Zenject)からの場合
- AutoSubstitute.zipをインポートする
- 同梱されているAutoSubstitute.zipを解凍する
- AutoSubstituteフォルダごとTestFrameworkフォルダに配置する
- Zenject-TestFramework.asmdefに
NSubstitute.dll
の参照を追加する
- AutoSubstitute.zipをインポートする
- nugetからの場合
- パッケージをダウンロード
- 拡張子をzipに書き換えて解凍
- それぞれ.NET Standard 2.1 or 2.0 のdllをプロジェクトに追加する
※今回はMoqのフォルダに入ってるものをそのまま使う - asmdefで、
AutoReference
のチェックを外し、Define Constraints
にUNITY_INCLUDE_TESTS
を追加する
- Extenject(Zenject)からの場合
基本的な使い方
- テストの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))); } }