セージ の メモ書き

メモこそ命の恩人だ

C# - Moq

Moq

  • テストダブル用のライブラリ。.NET用。
    • テストダブル (Test Double)、Double : 代役、影武者
    • テスト対象が依存するモジュールの代役となるオブジェクト。
    • スタブ、モック etc. の総称。
  • テストダブルの作成が手間なオブジェクトを用意できる。
    • DBアクセス、通信 etc.
  • Nuget より "Moq" をインストール。
    • 作成者:Daniel Cazzulino, kzu
    • ダウンロード数:305M (2022/7時点)
    • ライセンス:BSD 3-Clause License
    • 以下のライブラリもインストールされる。
      • Castle.Core (キャッスル・コア)
  • Moq の由来は、"Mock + LINQ" だと推測。

Mock クラス

  • Mock : 模造品
  • Mock クラスにより、指定した型のモックオブジェクトを生成できる。
  • Object プロパティでモックオブジェクトにアクセスする。
  • モックオブジェクトへのアクセス時の振る舞いを定義できる。
    • Setup・Returnsm メソッド etc.
var mock = new Mock<ISample1>();
var mockObject = mock.Object;

Debug.WriteLine($"{mock.GetType()},  {mockObject.GetType()}");
// Moq.Mock`1[ISample1],  Castle.Proxies.ISample1Proxy

メソッドのモック

  • Setup メソッド
    • モックするメソッドを指定する。
    • モックしたい引数の条件を定義できる。
      • It.IsAny()
      • It.Is(x => {...})
  • Returns メソッド
    • Setup で指定したメソッドの戻り値を定義する。
    • 戻り値のロジックも定義できる。
      • Returns(x => {...})
  • 以下のメソッドの場合、モックできる。
    • interface 内のメソッド
    • abstract メソッド
    • virtual メソッド
  • それ以外のオーバーライドできないメソッドは、モックできない。
    • Setup メソッドで例外が発生する。
    • System.NotSupportedException : Non - overridable members(here: ***) may not be used in setup / verification expressions.'

以下で試してみる。

public interface ISample1
{
    public int GetIntValue(int value);
    public bool GetBoolValue(bool value);
    public string GetStringValue(string value);
}
public class Sample1 : ISample1
{
    // 入力値をそのまま返すメソッド
    public int GetIntValue(int value) => value;
    public bool GetBoolValue(bool value) => value;
    public string GetStringValue(string value) => value;
}

GetIntValue メソッドをモックする。

// 1 を入力した場合、100 を返す。
var mock = new Mock<ISample1>();
mock.Setup(x => x.GetIntValue(1)).Returns(100);

Debug.WriteLine(mock.Object.GetIntValue(1));
// 100
Debug.WriteLine(mock.Object.GetIntValue(2));
// 0
    
// 指定した通りに動作することを確認。
// 入力が 1 以外の場合、戻り値型の初期値が返る。
// 任意の数値を入力した場合、100 を返す。
var mock = new Mock<ISample1>();
mock.Setup(x => x.GetIntValue(It.IsAny<int>())).Returns(100);

Debug.WriteLine(mock.Object.GetIntValue(1));
// 100
Debug.WriteLine(mock.Object.GetIntValue(2));
// 100

// 任意の数値で、100 が返ることを確認。
// 偶数を入力した場合、100 を返す。
var mock = new Mock<ISample1>();
mock.Setup(x => x.GetIntValue(It.Is<int>(value => value % 2 == 0))).Returns(100);

Debug.WriteLine(mock.Object.GetIntValue(1));
// 0
Debug.WriteLine(mock.Object.GetIntValue(2));
// 100
Debug.WriteLine(mock.Object.GetIntValue(3));
// 0
Debug.WriteLine(mock.Object.GetIntValue(4));
// 100

// 偶数の場合に、100 が返ることを確認。
// 任意の数値を入力した場合、偶数なら 100 を返し、それ以外は入力値を返す。
var mock = new Mock<ISample1>();
mock.Setup(x => x.GetIntValue(It.IsAny<int>()))
    .Returns<int>(value => (value % 2 == 0) ? 100 : value);

Debug.WriteLine(mock.Object.GetIntValue(1));
// 1
Debug.WriteLine(mock.Object.GetIntValue(2));
// 100
Debug.WriteLine(mock.Object.GetIntValue(3));
// 3
Debug.WriteLine(mock.Object.GetIntValue(4));
// 100

// 想定通りの値が返ることを確認。


Setup・Returns メソッドを複数定義してみる。

var mock = new Mock<ISample1>();
mock.Setup(x => x.GetIntValue(1)).Returns(100);
mock.Setup(x => x.GetBoolValue(false)).Returns(true);
mock.Setup(x => x.GetStringValue("sample")).Returns("(^^)/");

Debug.WriteLine(mock.Object.GetIntValue(1));
// 100
Debug.WriteLine(mock.Object.GetBoolValue(false));
// True
Debug.WriteLine(mock.Object.GetStringValue("sample"));
// (^^) /
// モックした通りに動作することを確認

Debug.WriteLine(mock.Object.GetIntValue(2));
// 0
Debug.WriteLine(mock.Object.GetBoolValue(true));
// False
Debug.WriteLine(mock.Object.GetStringValue("abc"));
// (null)
// 入力値が未定義の場合、戻り値型の初期値が返ることを確認。
// インターフェースだけで中身がないので、当然の動作である。

プロパティのモック

  • SetupGet メソッド
    • モックするプロパティを指定する。
    • Returns メソッドで戻り値を定義する。
    • モックしたプロパティが読取専用となる。
      • 値代入時、例外は発生しない。反映されないだけ。
  • SetupProperty メソッド
    • モックするプロパティを指定する。
    • 読取専用プロパティには使用できない。
    • モックしたプロパティの読み書きが許可される。
  • 以下のプロパティの場合、モックできる。メソッドと同様。
    • interface 内のプロパティ
    • abstract プロパティ
    • virtual プロパティ

以下で試してみる。

public interface ISample1
{
    public int IntValue { get; set; }
    public int ReadonlyIntValue { get; }
}
public class Sample1 : ISample1
{
    public int IntValue { get; set; } = 1;
    public int ReadonlyIntValue { get; } = 2;
}

SetupGet メソッドで、プロパティをモックする。

var mock = new Mock<ISample1>();
mock.SetupGet(x => x.IntValue).Returns(100);
mock.SetupGet(x => x.ReadonlyIntValue).Returns(200);

Debug.WriteLine(mock.Object.IntValue);
// 100
Debug.WriteLine(mock.Object.ReadonlyIntValue);
// 200
// モックした通りの値を返すことを確認

mock.Object.IntValue = 999;
Debug.WriteLine(mock.Object.IntValue);
// 100
// モックしたプロパティが読取専用のため、値が変化しないことを確認。

// mock.Object.ReadonlyIntValue = 1000;
// 読取専用のためビルドエラーになることを確認。

SetupProperty メソッドで、プロパティをモックする。

var mock = new Mock<ISample1>();
mock.SetupProperty(x => x.IntValue, 100);
//mock.SetupProperty(x => x.ReadonlyIntValue, 200);
// ReadonlyIntValue は読取専用のため、SetupProperty メソッドの場合は例外が発生する。

Debug.WriteLine(mock.Object.IntValue);
// 100
// モックした通りの値を返すことを確認

mock.Object.IntValue = 999;
Debug.WriteLine(mock.Object.IntValue);
// 999
// モックしたプロパティの値を変更できることを確認


xUnit の使用例

DB アクセス

public record Person(int Id, string Name);

public interface IDbRepository
{
    Person Load(int id);
    bool Save(Person person);
}

public class SampleClass1 
{
    private readonly IDbRepository dbRepository;
    public SampleClass1(IDbRepository dbRepository) => this.dbRepository = dbRepository;

    public bool Run(int personId) 
    {
        // IDに対応するデータの読込
         var loadedPerson = dbRepository.Load(personId);

        // 氏名を変更して保存
        var person = loadedPerson with { Name = $"{loadedPerson}さん" };
        return dbRepository.Save(person);
    }
}
  • SampleClass1 の Run メソッドをテストしてみる。
  • DB アクセスをモックにする。
[Fact]
public void DbRepositoryTest()
{
    var person = new Person(Id: 1, "テスト");
    var mock = new Mock<IDbRepository>();
    mock.Setup(x => x.Load(It.IsAny<int>())).Returns(person);
    mock.Setup(x => x.Save(It.IsAny<Person>())).Returns(true);

    var sample = new SampleClass1(mock.Object);
    var isSucceeded = sample.Run(personId: 999);

    Assert.True(isSucceeded);
    // テストが成功したことを確認。
    // Run メソッドが通り、true が返ることを確認。
}



以上