Unity 2021以降の場合は以下をどうぞ! tsgcpp.hateblo.jp
概要
今回は Moq というC#向けモックライブラリをUnityに導入する方法と、Moqを使ったテクニックをいくつか紹介しようと思います。
UnitTestにおいてモック化はかなり重要な立ち位置になると思いますが、自作モックは何かと保守コストが高かったり機能面が微妙になったりと悩みどころがあります。
そこでオススメなのが今回紹介する Moq となります。
他の言語のモックライブラリ同様、モック化だけでなくSpy機能(メソッドがコールされたかの確認機能)も結構充実していますので、
ぜひUnity もしくは C# のUnitTestに活かしていきましょう!
今回の記事はある程度UnitTestを知っていることが前提となりますが、UnitTestのサンプルコードにもできる限り説明を入れています。
チートシートにもなっていると思いますのでよかったら参考にしてください。
Moqの導入
Github
※今回はソースコードを直接取り込みません
MoqのソースコードはC# 8.0で書かれており、現在のLTSであるUnity 2019.4 ではコンパイルできません。
よって、後述するコンパイル済みのdllをダウンロードして使用することになります!
Moqの依存ライブラリとそのライセンスについて
使用する前に必ずライセンスを確認してください(特に業務で使用する場合は要注意です!)
- Moq: BSD 3-Clause License
- Castle.Core: Apache License Version 2.0
- Moqが依存するライブラリ
- System.Threading.Tasks.Extensions: MIT
- Moqが依存するライブラリ (Microsoft 提供)
- System.Runtime.CompilerServices.Unsafe: MIT
- System.Threading.Tasks.Extensionsが依存するライブラリ
上記4つを取り込む必要があります。
MoqをUnityに導入する方法
Moqおよび依存ライブラリのダウンロード
今回はMoqと依存ライブラリ3つをNugetからダウンロードしましょう。
以下の用に"Download package"からnupkgをダウンロード可能です。
nupkgを展開
nupkgの実体はzipファイルなので拡張子を ".zip" にするだけで大体のOSの標準で展開できると思います。
7-zipなどを使用すれば拡張子を変えなくても展開できると思います。
dllファイルの取り出し
展開したフォルダのうち、以下のパスにあるdllをUnityのAssets以下のフォルダにコピーしましょう。
Api Compatibility Level に従ってdll を選択してください。
.NET Standard 2.0
- Moq:
moq.4.X.X/lib/netstandard2.0/Moq.dll
- Castle.Core:
castle.core.4.X.X/lib/netstandard1.5/Castle.Core.dll
- .Net Standard2.0向けがないためこちらで代用
- System.Threading.Tasks.Extensions:
system.threading.tasks.extensions.4.X.X/lib/netstandard2.0/System.Threading.Tasks.Extensions.dll
- System.Runtime.CompilerServices.Unsafe:
system.runtime.compilerservices.unsafe.5.0.0/lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll
.NET 4.x or .NET Framework
- Moq:
moq.4.X.X/lib/net45/Moq.dll
- Castle.Core:
castle.core.4.X.X/lib/net45/Castle.Core.dll
- System.Threading.Tasks.Extensions:
system.threading.tasks.extensions.4.X.X/lib/net461/System.Threading.Tasks.Extensions.dll
- System.Runtime.CompilerServices.Unsafe:
system.runtime.compilerservices.unsafe.5.0.0/lib/net45/System.Runtime.CompilerServices.Unsafe.dll
Unityのプロジェクトへの配置は以下を参考にしてください
dllのプラットフォームの指定
Moqはあくまでテストのみに使用するため、ビルドする実行ファイルには含まれないように設定しておきましょう! 大抵のOSSは再頒布するとラインセス表記が必要なります。
特に Moq
と Castle.Core
は実行ファイルには不要なのでInclude Platforms
をEditor
のみにしておきましょう!
System.Threading.Tasks.Extensions と System.Runtime.CompilerServices.Unsafe のプラットフォーム指定について
System.Threading.Tasks.Extensions と System.Runtime.CompilerServices.Unsafe も不要であれば同様に設定してください。
ただ、ZLogger など一部メジャーなOSSも依存していたりするので、その場合は逆に設定しないように注意してください。
テスト用Assembly Definitionにdllを取り込み
以下を参考にテスト用Assembly Definitionの"Assembly References"に追加して"Apply"して下さい。
以上でMoqおよび依存ライブラリの取り込みは完了です!
Moqによるモック作成サンプル
せっかく取り込んでみたので、少し使い方を解説しようと思います。
モック化するinterface
// 戻り値有りのメソッドを持つinterface public interface IGetter { object Get(); } // プロパティを持つinterface public interface IHolder { object Value { get; } } // 引数を持つinterface public interface ILogger { void Log(string message); }
モック化の例
モックオブジェクトの作成
Mock<IGetter> mock = new Mock<IGetter>();
モックオブジェクトから元となる型を取り出し
var getterMock = new Mock<IGetter>(); // モックオブジェクトからIGetterインスタンスとして取り出し IGetter mockAsGetter = getterMock.Object;
メソッドの戻り値を指定
var getterMock = new Mock<IGetter>(); // Getメソッドのモック化("Hello Moq!"を返すように仕込み) getterMock.Setup(m => m.Get()).Returns("Hello Moq!");
プロパティの戻り値を指定
var holderMock = new Mock<IHolder>(); // Valueプロパティのモック化("Hi Moq!"を返すように仕込み) holderMock.Setup(m => m.Value).Returns("Hi Moq!")
特定のメソッドがコールされたかの確認(Spyとしての利用)
メソッドがコールされたかを検知するモックオブジェクトをSpyと呼ぶことがあります。
MoqのMockクラスは生成するだけでSpyとしても使用することが可能です!
また、「特定の引数が渡されたこと」、「任意の引数でコールされたこと」なども確認することが可能です。
// ILoggerのモックオブジェクトの作成 var loggerMock = new Mock<ILogger>(); var mockAsLogger = loggerMock.Object; // 複数回(3回)コール mockAsLogger.Log("One"); mockAsLogger.Log("Two"); mockAsLogger.Log("Two"); // 3回コールされたことの確認 loggerMock.Verify(m => m.Log(It.IsAny<string>()), Times.Exactly(3)); // 複数回(1回以上)コールされたことの確認 loggerMock.Verify(m => m.Log(It.IsAny<string>()), Times.AtLeastOnce()); // "One"でコールは1回であることの確認 loggerMock.Verify(m => m.Log("One"), Times.Once()); // "Two"でコールは2回であることの確認 loggerMock.Verify(m => m.Log("Two"), Times.Exactly(2));
Moqを使用したサンプルの全体
Moqの使用例の全体は以下のテストクラスで確認できます!
[https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqSample.cs
モック化するメソッド、プロパティの注意点
モック化するメソッドやプロパティはオーバーライド可能である必要があります。
オーバーライド不可の場合はモック化することはできません。。。
以下のMockTarget
クラス(非interface)の場合、FuncVirtual
とFuncAbstract
はモック化できますがFunc
はオーバーライド不可のためモック化できません(例外System.NotSupportedException
が飛びます)。
public abstract class MockTarget { public object Func() { return 1; } public virtual object FuncVirtual() { return 2; } public abstract object FuncAbstract(); }
Moqに限らずモックライブラリ全般に言えますが、モック化の対象は基本的にinterfaceを使用することをオススメします!
https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqException.cs
Moqを使ったTips集
in, ref指定の構造体を引数として持つメソッドの任意の引数でのVerify
以下のように引数にin
が指定されたメソッドのことです。
public interface IInCaller { void Call(in Parameter parameter); }
ちなみにParameterは以下です。
public struct Parameter { public int x; }
さて、このin
が指定されたメソッドのVerifyですが、なんとIt.IsAny<T>()
だとSpyが機能しなくなります。。。
target.Verify(m => m.Call(It.IsAny<Parameter>()), Times.Once());
in
やref
が指定されている場合は以下の It.Ref<T>.IsAny
を使用しましょう!
target.Verify(m => m.Call(It.Ref<Parameter>.IsAny), Times.Once());
Moq自体の仕様なのでご注意ください。
細かい使用確認は以下のテストコードで実施しています。 よかったらこちらもどうぞ!
https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqRefVerify.cs
in, refどちらも指定されていない場合
It.IsAny<T>()
で確認しましょう。
こちらは逆にIt.Ref<T>.IsAny
が機能しなくなります。
指定ミスの対策
コンパイルエラーにならない関係でヒューマンエラーが起きそうですよね?
特にTimes.Never()
を使用する場合、コールなし or 指定ミス どちらなのか分からなくなってしまいます。
target.Verify(m => m.Call(It.Ref<Parameter>.IsAny), Times.Never());
オススメなのはIt.IsAny<T>()
もしくは It.Ref<T>.IsAny
を用いるテストを実施する場合は、
正常系のテストに以下のTimes.Once()
など "1回以上コールされたことの検証を混ぜ込むことです。
target.Verify(m => m.Call(It.Ref<Parameter>.IsAny), Times.Once());
Times.Once()
やTimes.Exactly(N)
でテストが通る = 機能している ということになりますので!
Callbackの活用例
Callbackの紹介
Moqにはモックオブジェクトのメソッドがコールされたときにコールバック(イベント)を仕込むことが可能です。
// コールバック時のパラメータ格納用List var messageList = new List<string>(); // ILoggerのモックオブジェクトの作成 var loggerMock = new Mock<ILogger>(); // モックオブジェクトにコールバックを仕込む loggerMock .Setup(m => m.Log(It.IsAny<string>())) .Callback<string>(message => messageList.Add(message));
上記のCallback
はLog
メソッドに渡された引数をコールバックにより取り出すことが可能になります。
MoqのCallbackでUnityのExecutionOrderとUniTaskの実行順序の検証
さてコードを書いていると時々「絶対この順番でメソッドがコールされていることを保証したい」ということ有りませんか?
そんなときにCallbackを活用するとテストにコール順の検証を組み込むことができます(Moqを使用するので各メソッドがモック化可能なことが前提となります)。
今回はCallbackを活用して、UnityのExecutionOrderとUniTaskを混合した実行順序のテスト化をやってみたいと思います。
具体的にはDefaultExecutionOrder
を指定したコンポーネント, UniTask.Yield
に指定したPlayerLoopTimingそれぞれが同時に存在する場合に実行順序を確認するテストを作成します。
確認用コンポーネントとテストコードについて
IRunner
型のインスタンスを渡し、Run()
メソッドをコールさせる構成を取ります。
DefaultExecutionOrderを指定したコンポーネント
DefaultExecutionOrder
を指定する以外は共通のためベースクラスを作成し、それを継承したクラスを用意しました。
以下は int.MaxValueを指定した場合の例です。
using UnityEngine; /// <summary> /// MonoBehaviourによるRunner実行 (order: int.MaxValue) /// </summary> [DefaultExecutionOrder(int.MaxValue)] public class ProcessorOrderMaxValue : BaseProcessOrder { } /// <summary> /// MonoBehaviourによるRunner実行のベース /// </summary> public abstract class BaseProcessOrder : MonoBehaviour, IProcessor { public IRunner Runner { get; set; } = null; public IRunner LateRunner { get; set; } = null; protected virtual void Update() { Runner?.Run(); } protected virtual void LateUpdate() { LateRunner?.Run(); } }
UniTaskを使用したクラス
using UnityEngine; using Cysharp.Threading.Tasks; /// <summary> /// UniTaskによるRunner実行 /// </summary> public class ProcessorUniTask : IProcessor { public IRunner Runner { get; set; } = null; // 未使用 public IRunner LateRunner { get; set; } = null; public PlayerLoopTiming Timing { get; set; } = PlayerLoopTiming.Update; /// <summary> /// Runnerの非同期実行 /// </summary> public void Process() { ProcessAsync().Forget(); } private async UniTask ProcessAsync() { await UniTask.Yield(Timing); Runner?.Run(); } }
テストコード
結構コードが長くなってしまったので全体は以下のGithubのコードをご確認ください。
大まかに言うとIRunner
のモックオブジェクトを各コンポーネントごとに作成し、対象のコンポーネントごとにコールバック時のメッセージを仕込みます。
各コンポーネント内のUpdate()
, LateUpdate()
および UniTaskのawait後にIRunner.Run()
がコールされると順番にリスト_callbackMessageList
にメッセージが追加されていきます。
var runnerMock = new Mock<IRunner>(); // MockのIRunner.Run()がコールされた際にメッセージを登録するように仕込む(いわゆるSpy) runnerMock .Setup(mock => mock.Run()) .Callback(() => _callbackMessageList.Add(message))
最後に1フレーム動かして、_callbackMessageList
にAddされたメッセージの順番を確認することで実行順序を確認します。
yield return new WaitForEndOfFrame() Assert.AreEqual(18, _callbackMessageList.Count); Assert.AreEqual("UniTask PlayerLoopTiming.PreUpdate", _callbackMessageList[0]); Assert.AreEqual("UniTask PlayerLoopTiming.LastPreUpdate", _callbackMessageList[1]); Assert.AreEqual("UniTask PlayerLoopTiming.Update", _callbackMessageList[2]); Assert.AreEqual("ProcessorOrderMinValue Update", _callbackMessageList[3]); Assert.AreEqual("ProcessorOrderM1 Update", _callbackMessageList[4]); // 以下略
TestRunnerを実行してみました。
問題なさそうですね。
このテストを組み込んでおくことでUnityやUniTaskの更新で実行順序が変わった場合に検知することができたりはするかなと思います。
「Debug.Logで良くない?」と言われるとその通りなんですけどねw
個人的に詳細な実行順序を調べたかったというのもあったので、ついでにCallbackに使用する題材にしてみました。
サンプルプロジェクト
雑感
1ヶ月半ぶりの投稿です。
ネタはいっぱいあるんですが、今やってることに熱中しちゃってなかなかブログを書けませんでしたw。
次こそは3DCG系のネタをやりたいかなと思います。
なんかブログを出すこと自体が目的になってるようなとこがあるので、焦らずやっていこうかなと思います。
それでは~