Moq 実装メモ


Moq 実装メモ

C# のテスト実装で Moq を使ったのでメモ。

環境

  • Visual Studio 2019 のプロジェクトテンプレート (NUnit テストプロジェクト (.NET Core)) を使用
  • NuGet パッケージから Moq をインストール

テスト対象のクラス実装

あくまで用例のための実装であり、 Moq が活きるテスト対象ではない。

Human.cs
public class Human
{
    /// <summary>
    /// 苗字
    /// </summary>
    private string FamilyName { get; }

    /// <summary>
    /// 名前
    /// </summary>
    private string GivenName { get; }

    /// <summary>
    /// 年齢
    /// </summary>
    public virtual int Age { get; }

    public Human(string familyName, string givenName, int age)
    {
        FamilyName = familyName;
        GivenName = givenName;
        Age = age;
    }

    /// <summary>
    /// フルネームを作成する。
    /// </summary>
    /// <returns>フルネーム</returns>
    protected virtual string CreateFullName()
    {
        return $"{FamilyName} {GivenName}";
    }

    /// <summary>
    /// 年齢付きのフルネームを作成する。
    /// </summary>
    /// <returns>年齢付きのフルネーム</returns>
    public virtual string CreateFullNameWithAge()
    {
        return $"{CreateFullName()} {Age}";
    }

    /// <summary>
    /// 年齢とその単位付きのフルネームを作成する。
    /// </summary>
    /// <returns>年齢とその単位付きのフルネーム</returns>
    public virtual string CreateFullNameWithAge(string ageUnit)
    {
        return $"{CreateFullNameWithAge()}{ageUnit}";
    }
}

注意点

テスト対象クラスを実装する上での注意点。

virual

モック対象のメソッドはオーバーライド可能な状態にする必要があるため virtual 修飾子が必須となる。可視性も protected 以上。

以下はオーバーライド不能なメソッドをモック化しようとしたときのエラー文。 (一時的に CreateFullName() メソッドから virtual 修飾子を除去して実行)

System.NotSupportedException : Unsupported expression: mock => mock.CreateFullName()
    Non-overridable members (here: Human.CreateFullName) may not be used in setup / verification expressions.

static

静的なメソッドやプロパティ等はモック化できない。

テスト実装

テスト実装項目で使用するクラスの構造。

Tests.cs
public class Tests
{
    /// <summary>
    /// 苗字 (テスト用の固定値)
    /// </summary>
    private string FamilyName { get; set; }

    /// <summary>
    /// 名前 (テスト用の固定値)
    /// </summary>
    private string GivenName { get; set; }

    /// <summary>
    /// 年齢 (テスト用の固定値)
    /// </summary>
    private int Age { get; set; }

    /// <summary>
    /// テストの共通設定。
    /// </summary>
    [SetUp]
    public void Setup()
    {
        FamilyName = "苗字";
        GivenName = "名前";
        Age = 20;
    }

    // 以下にテスト実装を記述
}

基本

Moq を用いたモック化では Moq.Mock<T> クラスを用いる。

new 時は <T> 部にモック化対象のクラスを設定し、引数にはモック化対象のコンストラクタのメソッドを設定する。

モック化されたオブジェクトは Moq#Object から呼び出す。

var humanMock = new Mock<Human>(FamilyName, GivenName, Age)
humanMock.Object.CreateFullNameWithAge();

CallBase = false

Mock クラスで最初につまずいたのは CallBase プロパティの扱い。

この値は、初期状態では false が設定されている。

次の例では CallBasefalse のため、モック未設定のメソッドは null 返却となる。 (メソッド自体も呼び出されない)

CallBase_False
   [Test]
   public void CallBase_False()
   {
       var humanMock = new Mock<Human>(FamilyName, GivenName, Age);
       Assert.AreEqual(null, humanMock.Object.CreateFullNameWithAge());
   }

CallBase = true

次の例では CallBasetrue のため、モック未設定のメソッドは本来のメソッドが呼び出される。

CallBase_True
   [Test]
   public void CallBase_True()
   {
       var humanMock = new Mock<Human>(FamilyName, GivenName, Age) { CallBase = true };
       Assert.AreEqual($"{FamilyName} {GivenName} {Age}", humanMock.Object.CreateFullNameWithAge());
   }

その他

  • Moq#Object はモック化対象と同一クラス。テストメソッド内で直接利用するだけでなく、引数やフィールドを通して間接的に利用することも可能。

プロパティのモック化

Mock#SetupGet メソッドの Func 型引数からモック化対象のプロパティを指定し、その返却オブジェクトの Returns メソッドからモック化対象の返却値を設定する。

モック化されたプロパティは、設定された返却値を常に返すようになる。

OverrideProperty_Age
   public void OverrideProperty_Age()
   {
       var humanMock = new Mock<Human>(FamilyName, GivenName, Age) { CallBase = true };
       humanMock.SetupGet(m => m.Age).Returns(9999);

       Assert.AreEqual($"{FamilyName} {GivenName} 9999", humanMock.Object.CreateFullNameWithAge());
       Assert.AreEqual($"{FamilyName} {GivenName} 9999才", humanMock.Object.CreateFullNameWithAge("才"));
   }

public なクラスメソッドのモック化

Setup(Func) のモック化対象メソッドの引数が型指定のみ

Mock#Setup メソッドの Func 型引数からモック化対象のメソッドを指定し、その返却オブジェクトの Returns メソッドからモック化対象の返却値を設定する。

Func 型引数で設定するメソッドには Moq.It<T> クラスを利用するなどして、モック化するメソッドを引数構成込みで明示的に設定する。

OverrideMethod_CreateFullNameWithAgeUnit
    /// <summary>
    /// 引数付きの CreateFullNameWithAge を上書きする。
    /// </summary>
    [Test]
    public void OverrideMethod_CreateFullNameWithAgeUnit()
    {
        var humanMock = new Mock<Human>(FamilyName, GivenName, Age) { CallBase = true };
        humanMock.Setup(m => m.CreateFullNameWithAge(It.IsAny<string>())).Returns("上書き");

        // Setup で指定した引数構成のものがモック化されている
        // 本来のメソッドは呼び出されず Returns で設定した戻り値が返却されている
        Assert.AreEqual("上書き", humanMock.Object.CreateFullNameWithAge("歳"));

        // Setup で指定したメソッドと同名だが、引数構成が異なる
        // モック化の対象外となるため、本来のメソッドが呼び出されている
        Assert.AreEqual($"{FamilyName} {GivenName} {Age}", humanMock.Object.CreateFullNameWithAge());
    }

Setup(Func) のモック化対象メソッドの引数が固定値

Moq.It<T> を使わず、固定値を設定することもできる。

OverrideMethod_CreateFullNameWithAgeUnit_2
    /// <summary>
    /// 引数付きの CreateFullNameWithAge を上書きする。
    /// </summary>
    [Test]
    public void OverrideMethod_CreateFullNameWithAgeUnit_2()
    {
        var humanMock = new Mock<Human>(FamilyName, GivenName, Age) { CallBase = true };
        humanMock.Setup(m => m.CreateFullNameWithAge("歳")).Returns("上書き");

        // Setup で指定したメソッド名と引数が一致している
        // モック化により本来のメソッドが呼び出されず Returns で設定した戻り値が返却されている
        Assert.AreEqual("上書き", humanMock.Object.CreateFullNameWithAge("歳"));

        // Setup で指定したメソッドと同名、かつ、同じ型の引数構成だが、正確な設定値が異なる
        // モック化の対象外となるため、本来のメソッドが呼び出されている
        Assert.AreEqual($"{FamilyName} {GivenName} {Age}才", humanMock.Object.CreateFullNameWithAge("才"));
    }

その他メモ

  • 非同期メソッドは ReturnsAsync を利用可能。

protected なクラスメソッドのモック化

引数なしメソッドのモック化

Moq#Protected メソッドの返却オブジェクトの Setup<T> メソッドからモック化の設定を行う。

<T> 部には戻り値の型を設定し、第 1 引数にモック化対象のメソッド名を設定する。

Mock_ProtectedMethod
    /// <summary>
    /// protected メソッドのモック化。
    /// </summary>
    [Test]
    public void Mock_ProtectedMethod()
    {
        var humanMock = new Mock<Human>(FamilyName, GivenName, Age) { CallBase = true };
        humanMock.Protected().Setup<string>("CreateFullName").Returns("上書き");

        // 上書きした戻り値が返却されている
        Assert.AreEqual($"上書き {Age}", humanMock.Object.CreateFullNameWithAge());
    }

引数ありメソッドのモック化

Setup<T> の第 2 以降の引数に ItExpr.IsAny<T>() 等を用いて引数構成を設定する。

MockProtectedMethod_WithArgs
humanMock.Protected().Setup<string>("GetAgeWithUnit", ItExpr.IsAny<string>()).Returns("上書き");

メソッドの実行

通常のテスト実装で、モック化したメソッドが常にテストされないことは珍しいと思う。

Moq の範囲ではないが、可視性が public でないメソッドのリフレクション実行の方法も記述しておく。

Run_ProtectedMethod
    [Test]
    public void Run_ProtectedMethod()
    {
        var human = new Human(FamilyName, GivenName, Age);
        Type type = human.GetType();
        MethodInfo methodInfo = type.GetMethod("CreateFullName", BindingFlags.Instance | BindingFlags.NonPublic);

        // 引数付きメソッドの場合、第二引数は object[] 型の値を渡す
        Assert.AreEqual($"{FamilyName} {GivenName}", methodInfo.Invoke(human, null));
    }

インターフェイスメソッドのモック化

上記まではクラスメソッドについての記述となる。

テストのためだけにメソッドをオーバーライド可能な状態にするのに違和感を覚えていたが、 @naminodarie さんのコメントにより、インターフェイスを使えばオーバーライドを避けられると知った。

IHuman.cs
/// <summary>
/// テスト対象クラスのインターフェイス。
/// </summary>
public interface IHuman
{
    /// <summary>
    /// 年齢付きのフルネームを作成する。
    /// </summary>
    /// <returns>年齢付きのフルネーム</returns>
    string CreateFullNameWithAge();

    /// <summary>
    /// 年齢とその単位付きのフルネームを作成する。
    /// </summary>
    /// <returns>年齢とその単位付きのフルネーム</returns>
    string CreateFullNameWithAge(string ageUnit);
}
Mock_Interface
    [Test]
    public void Mock_Interface()
    {
        var humanMock = new Mock<IHuman>();

        // モック化の設定はクラスをもとにした場合と同様
        humanMock.Setup(m => m.CreateFullNameWithAge(It.IsAny<string>())).Returns("上書き");

        // Setup で指定した引数設定と同一のため、呼び出されず Returns で設定した戻り値が返却されている
        Assert.AreEqual("上書き", humanMock.Object.CreateFullNameWithAge("歳"));
    }

仮実装や、特定環境でフェイクオブジェクトを使う際に特に効果を発揮しそうな予感。

その他メモ

ただし、下記の目的では使えない。

  • メソッドから呼び出されるメソッドのモック化。
  • protected なメソッドのモック化。

雑感

  • 便利。
  • 実際モック化で使うクラスは Mock なのか。
  • 静的メソッドを使用している処理まわりのテストはどのようにするのが適切だろう。静的メソッドの使用箇所は、メソッドの途中に記載するのではなく専用のメソッドを用意すべきだろうか。