すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【Moq】UnityでのMoq導入方法 と MoqのTips集を紹介!

Unity 2021以降の場合は以下をどうぞ! tsgcpp.hateblo.jp

概要

今回は Moq というC#向けモックライブラリをUnityに導入する方法と、Moqを使ったテクニックをいくつか紹介しようと思います。

UnitTestにおいてモック化はかなり重要な立ち位置になると思いますが、自作モックは何かと保守コストが高かったり機能面が微妙になったりと悩みどころがあります。
そこでオススメなのが今回紹介する Moq となります。

他の言語のモックライブラリ同様、モック化だけでなくSpy機能(メソッドがコールされたかの確認機能)も結構充実していますので、
ぜひUnity もしくは C# のUnitTestに活かしていきましょう!

今回の記事はある程度UnitTestを知っていることが前提となりますが、UnitTestのサンプルコードにもできる限り説明を入れています。
チートシートにもなっていると思いますのでよかったら参考にしてください。

Moqの導入

Github

※今回はソースコードを直接取り込みません

github.com

MoqのソースコードC# 8.0で書かれており、現在のLTSであるUnity 2019.4 ではコンパイルできません。

よって、後述するコンパイル済みのdllをダウンロードして使用することになります!

Moqの依存ライブラリとそのライセンスについて

使用する前に必ずライセンスを確認してください(特に業務で使用する場合は要注意です!)

上記4つを取り込む必要があります。

MoqをUnityに導入する方法

Moqおよび依存ライブラリのダウンロード

今回はMoqと依存ライブラリ3つをNugetからダウンロードしましょう。

以下の用に"Download package"からnupkgをダウンロード可能です。

www.nuget.org

www.nuget.org

www.nuget.org

www.nuget.org

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は再頒布するとラインセス表記が必要なります。

特に MoqCastle.Coreは実行ファイルには不要なのでInclude PlatformsEditorのみにしておきましょう!

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)の場合、FuncVirtualFuncAbstractはモック化できますが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());

inrefが指定されている場合は以下の 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));

上記のCallbackLogメソッドに渡された引数をコールバックにより取り出すことが可能になります。

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のコードをご確認ください。

https://github.com/tsgcpp/UnityMoqSample/blob/main/Assets/Tests.UniTask/ExecutionOrder/TestExecutionOrder.cs

大まかに言うと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に使用する題材にしてみました。

サンプルプロジェクト

github.com

雑感

1ヶ月半ぶりの投稿です。

ネタはいっぱいあるんですが、今やってることに熱中しちゃってなかなかブログを書けませんでしたw。

次こそは3DCG系のネタをやりたいかなと思います。

なんかブログを出すこと自体が目的になってるようなとこがあるので、焦らずやっていこうかなと思います。

それでは~