すぎしーのXRと3DCG

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

UnityでMoqを使う (Unity2021バージョン)

こちらは クラスター Advent Calendar 2022(2ページ目)の17日目の記事です!

前日はスワンマンさん (@Swanman) の「Unityのエディタ拡張で動的にメニューを追加・削除する」でした!

まさかエンジニアではなくカスタマーサポートの方からReflectionを使ったツールの作り方を教えてもらえるとは!
Unity上でツールを作るときに知っておくと便利なテクニックになると思いますのでぜひ参考にしてください。


こんにちは、すぎしーです。 クラスター株式会社のUnityエンジニアをなりました!

改めてよろしくお願いします。

概要

今回の内容は2年前に書いた 「UnityでのMoq導入方法」のUnity2021版です。
この2年でUnityもMoqもアップデートされているので、導入も前回より内容を強化した方法で紹介します!

記事の最後の方に、導入までをある程度自動化した方法も載せておきます。

ソフトウェアエンジニア向け の記事になります。

変更履歴

  • 2022/12/18 .NET Framework向けの依存dllを追加 及び "Moq 4.18.2以上にする理由"の説明を一部修正

Moqとは

Moqとは C#(.Net) 向けのモックオブジェクト作成ライブラリです。

モッククラスはUnitTestなどで依存interfaceと同じふるまい(モック)になるクラスですが、自作で実装するのはなかなか骨が折れる作業になります。
そんなときにモックライブラリを用いることで簡単にモックオブジェクトを用意でき、より高度なUnitTest (クラスの単体テスト) が可能になります。


UnityでMoqを導入

※Unity2020でも可能と思いますが、Unity2021以上推奨です!

最初に手作業でのやり方紹介します。

1. MoqとCastle.Coreのnupkgをダウンロード

NuGetからnupkgをダウンロードします。

Moq 4.18.2以上にする理由は後述します。

ダウンロードはページ横の "Download package" から可能です。

2. nupkgを展開

nupkgの実態はzipなので7-zipなどで直接展開できます。
拡張子を .zip に変えてOS標準のzip展開でも可能です。

3. dll を Unityプロジェクト内に配置

展開したファイルのうち、以下のファイルをUnity以下に配置しましょう。
個人的なオススメのフォルダは Plugins/Moq です。

  • "Api Compatibility Level" が ".Net Standard 2.1"の場合
    • moq.4.18.3/lib/netstandard2.1/Moq.dll
    • castle.core.5.1.0/lib/netstandard2.1/Castle.Core.dll
    • system.diagnostics.eventlog.7.0.0/lib/netstandard2.0/System.Diagnostics.EventLog.dll
  • "Api Compatibility Level" が ".Net Framework"の場合
    • moq.4.18.3/lib/net462/Moq.dll
    • castle.core.5.1.0/lib/net462/Castle.Core.dll
    • system.threading.tasks.extensions.4.5.4/lib/net461/System.Threading.Tasks.Extensions.dll
    • system.runtime.compilerservices.unsafe.6.0.0/lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll

以下のように配置してください。

4. Unity上でdllをTestRunner向けに調整

Moqはあくまでテスト用なので、ビルドしたアプリには含まれないように設定しましょう。

※ビルドしたアプリに混ぜる場合は再頒布となるため各dllのライセンス表記が必要となります。

以下は Moq.dll, Castle.Core.dll, System.Diagnostics.EventLog.dll全てで実施してください。

  • Inspectorを表示
  • Auto Reference を無効化
  • Validate References を有効化
  • "Define Constraints" に UNITY_INCLUDE_TESTS を指定
  • "Apply" ボタンをクリック

特に UNITY_INCLUDE_TESTS を指定することでEditMode及びPlayMode Test Runnerで使用できる状態で、
ビルドしたアプリ本体にはMoqと依存dllが除外されます。

以上で導入は完了です!


UnityでMoqを使用

次は導入したMoqを使ってみましょう!

Unity Test Framework の詳細は省略します

テスト用Assembly Definitionを作成

  • テスト用スクリプトを配置したいフォルダで右クリック
  • Create -> Testing -> Tests Assembly Folder をクリック
  • Assembly名を指定してasmdefを作成

テスト用Assembly DefinitionにMoqの参照を追加

  • テスト用asmdefのInspectorを開く
  • "Assembly References"に Moq.dll を追加
    • Castle.Core.dllSystem.Diagnostics.EventLog の指定は基本的に不要 (テスト用スクリプトで直接参照することは稀なため)

テストを書いて実行

あとは普段どおりテストスクリプトを作成して、Test Runnerで実行するだけです!
Moqを使った簡易的なテストコードの例を載せておきます。

using System.Collections.Generic;
using NUnit.Framework;
using Moq;

public class TestFuncProxy
{
    public interface IFunc
    {
        bool Invoke(int number);
    }

    [Test]
    public void Invoke_ReturnsFalse_IfFuncReturnsFalse()
    {
        // Arrange
        var mock = new Mock<IFunc>();
        var target = new FuncProxy(mock.Object);

        // Note: Moqの仕様でSetupなしの場合はdefaultを返す (bool Invoke(...) の場合はfalse)
        // FYI: 実際のテストではテストパターンを明確にするために明示しましょう!

        // Act
        bool actual = target.Invoke(3);

        // Assert
        Assert.That(actual, Is.False);
    }

    [Test]
    public void Invoke_ReturnsTrue_IfFuncReturnsTrue()
    {
        // Arrange
        var mock = new Mock<IFunc>();
        var target = new FuncProxy(mock.Object);

        // Note: 引数3を渡されたらtrueを返す
        mock.Setup(m => m.Invoke(3)).Returns(true);

        // Act
        bool actual = target.Invoke(3);

        // Assert
        Assert.That(actual, Is.True);
    }
}

以下は実行結果です。

使用方法の紹介は以上です!

Moqの使用例

Moqでできることをちょっと紹介します!

SetupSequenceでコールごとの挙動を指定

SetupSequence で指定するとコールごとの戻り値を指定できます。

    [Test]
    public void Example_SetupSequence()
    {
        var mock = new Mock<IFunc>();

        // 渡された引数に関係なくfalse -> true -> false -> throw Exception
        mock.SetupSequence(m => m.Invoke(It.IsAny<int>()))
            .Returns(false)
            .Returns(true)
            .Returns(false)
            .Throws(new System.Exception("Unexpected Call"));

        Assert.That(mock.Object.Invoke(default), Is.False);
        Assert.That(mock.Object.Invoke(default), Is.True);
        Assert.That(mock.Object.Invoke(default), Is.False);
        Assert.Throws<System.Exception>(() => mock.Object.Invoke(default));
    }

コール時の引数と回数の検査

Verify を使用するとコールされたときの引数やその引数でのコール回数を検査することができます。
Moqを使う場合は一番使う機能ではないかと!

    [Test]
    public void Example_Verify()
    {
        var mock = new Mock<IFunc>();

        mock.Object.Invoke(2);
        mock.Object.Invoke(5);
        mock.Object.Invoke(2);

        // 引数2で2回コールされたことの検証
        mock.Verify(m => m.Invoke(2), Times.Exactly(2));

        // 引数関係なく3回以上コールされたことの検査
        mock.Verify(m => m.Invoke(It.IsAny<int>()), Times.AtLeast(3));
    }

不正の場合は例外(MockException)が出て、テストが失敗します。

コール時の処理を設定

Callback を使用するとコールされたときの処理を設定できます。
複数オブジェクトのコールされた順番を検査するときなどに利用できます。

    [Test]
    public void Example_Callback()
    {
        var messageList = new List<string>();

        var mock1 = new Mock<IFunc>();
        var mock2 = new Mock<IFunc>();
        var mock3 = new Mock<IFunc>();

        // コールされたら messageList に文字列を追加
        mock1.Setup(m => m.Invoke(It.IsAny<int>())).Callback(() => messageList.Add("From 1"));
        mock2.Setup(m => m.Invoke(It.IsAny<int>())).Callback(() => messageList.Add("From 2"));
        mock3.Setup(m => m.Invoke(It.IsAny<int>())).Callback(() => messageList.Add("From 3"));

        mock3.Object.Invoke(0);
        mock1.Object.Invoke(0);
        mock2.Object.Invoke(0);
        mock1.Object.Invoke(0);

        // 合計のコール回数 及び コールされた順番を検査
        Assert.That(messageList.Count, Is.EqualTo(4));
        Assert.That(messageList[0], Is.EqualTo("From 3"));
        Assert.That(messageList[1], Is.EqualTo("From 1"));
        Assert.That(messageList[2], Is.EqualTo("From 2"));
        Assert.That(messageList[3], Is.EqualTo("From 1"));
    }


モックライブラリを使用するメリット

改めてモックライブラリを使うメリットを紹介します。

モックオブジェクトを簡単に生成可能

これまで説明した通りですが、モッククラスを独自に実装する必要がなくなります。

interface を使ったモッククラスを自作する場合は結構な行数を書くことになり、何より保守コストが発生して大変です。
テストの品質を考える場合はモッククラスのテストも必要になってきます。

ちなみにちょっと実装してみましたが、実際に自作する場合はもっと機能が必要になっていきます。

using System.Linq;
...
    public class MyMockFunc : IFunc
    {
        // コールされた戻り値の設定 (直近のコールのみ)
        public bool RetNumber { get; set; }

        // コールされたときの引数の格納用リスト
        public List<int> CallHistory = new();

        // 対象の引数でコールされた回数の検査
        public bool Verify(int targetNumber, int expected)
            => CallHistory.Where(number => number == targetNumber).Count() == expected;

        // メソッドをコールされたときの処理
        public bool Invoke(int number)
        {
            CallHistory.Add(number);
            return RetNumber;
        }
    }

特別な事情がない限り、早めにモックライブラリを導入しておくとUnitTestが億劫にならなくて良いかと思います。

IDEのリファレンス検索に余計な候補がでない

モックライブラリ使う場合はモッククラスを実装するわけではないので、IDEのinterface継承クラスの検索結果にモッククラスが並びません。

以下はモッククラスが定義されたプロジェクトでinterface継承クラスの検索結果イメージです。

(interfaceがGeneric型だった場合はさらに大変なことに)

高度な検証がより簡単に実現可能

「Moqの使用例」で紹介した通り、モックライブラリを使用すると複雑な検証もやりやすくなります。

  • 特定の引数で依存クラスのメソッドをN回コールすること
  • 依存クラスが OperationCanceledException を返すときにはエラーにならないこと
  • etc...


モックライブラリの導入はUnitTest自体の敷居を下げることができるので、是非活用してみてください。


簡易導入方法

以下にある程度自動化した方法を紹介しています。

github.com

オススメは実施環境に依存しない 「GitHub Acrtionsを使用する場合」 です! (Actionsのyaml設定ファイルも作成済みです)

生成されたフォルダをそのまま Assets 以下に配置すれば使用できる状態になっています。


余談

Moq 4.18.2以上にする理由

理由は".Net Standard 2.1"の依存するライブラリが削減されており、導入がより簡単になるためです。

実は4.18.1以前では System.Threading.Tasks.Extensions とその依存 System.Runtime.CompilerServices.Unsafe も一緒に入れる必要がありましたが、 4.18.2で依存が削除されました。

Removed dependency on System.Threading.Tasks.Extensions for netstandard2.1 and net6.0 (@tibel, #1274)

moq4/CHANGELOG.md at main · moq/moq4 · GitHub

追記

".NET Framework 4.x"では System.Threading.Tasks.Extensions の依存は残っているため引き続き必要となるようです。
簡易導入方法で紹介しているツールも修正済みです。


雑感

実装したクラスすべてにUnitTestが必要になるわけでは有りませんが、
恒久的に機能を保証したい場合などは強力な武器になるので、Unity開発でもMoqを活用してみてください。

クラスター株式会社にジョインして業務にも慣れてきましたが、
エンジニアに限らず様々な分野のスペシャリストやジェネラリストの方がいて、刺激的な日々を送っています。

これからもバーチャルにのめり込んでいきます!


クラスター Advent Calendar 2022 明日の記事の紹介

明日は Soraさん (@BlueRose_Sora) の「Tips:clusterで大規模な展示会をする」です!

お楽しみに!