NDW

アプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

.NET Core 1. システムエンジニアリング 実装技術

.NET Core(C#): Moqを使ったモック作成方法

投稿日:2021年8月11日 更新日:

はじめに

  • 次の環境を使用して動作確認しています。
    OS Windows 10(64ビット)
    IDE Microsoft Visual Studio Community 2019(16.8.5) + C#(8.0)
    パッケージ Microsoft.NET.Test.Sdk 16.10.0
    xunit 2.4.1
    xunit.runner.visualstudio 2.4.3
    Moq 4.16.1
  • 完全なソースコードはこちらで公開しています。
  • xUnitのAssertの使い方はこちらで紹介しています。

基本的なモック

基本的なモックの例を次に示します。

var targetMock = new Mock<ITarget>();
ITarget target = targetMock.Object;

// 特定引数に対して固定値を返却するモック
// モック対象: public int Add(int x, int y)
targetMock.Setup(o => o.Add(1, 2)).Returns(3);
// テスト
Assert.Equal(3, target.Add(1, 2));
Assert.Equal(0, target.Add(9, 9)); // 引数不一致の場合は既定値になる

// 特定引数に対して固定値を返却するモック(非同期)
// モック対象: public Task<int> AddAsync(int x, int y)
targetMock.Setup(o => o.AddAsync(1, 2)).ReturnsAsync(3);
// テスト
Assert.Equal(3, await target.AddAsync(1, 2));
Assert.Equal(0, await target.AddAsync(9, 9)); // 引数不一致の場合は既定値になる

// 任意引数に対して固定値を返却するモック
// モック対象: public bool Send(string message)
targetMock.Setup(o => o.Send(It.IsAny<string>())).Returns(true);
// テスト
Assert.True(target.Send("abc"));
Assert.True(target.Send("12345"));
  • モック化するオブジェクトのインターフェイスを仮引数に指定してMockオブジェクトを生成します。
  • MockオブジェクトのSetup系メソッドでプロパティやメソッド呼び出しの条件を指定します。
    条件として任意の引数を指定する場合は、It.Any<型>()を指定します。
  • Returns系メソッドで戻り値を指定します。
  • 実行時にモックとして指定したプロパティやメソッドの引数と合致しない場合、戻り値の型に応じた既定値が返却されます。(int型なら0、string型ならnullなど)
  • テストで使用する実際のモック化されたオブジェクトは、MockオブジェクトのObjectプロパティから取得します。

引数に基づいた値を返却するモック

複数のSetupの定義やラムダ式を使用して、引数に基づいた値を返却できます。
次の例では、Measure1(), Measure2()ともに、引数に基づいて”matched”, “near”, “far”のいずれかを返却します。Measure1()は複数のSetup()で、Measure2()はラムダ式で、同一条件を定義しています。

var targetMock = new Mock<ITarget>();
ITarget target = targetMock.Object;

// 引数に応じた値を返却するモック(1)
// ※Setupの条件一致が重複する場合は後勝ち
// モック対象: public string Measure1(int str)
targetMock.Setup(o => o.Measure1(It.IsAny<int>())).Returns("far");
targetMock.Setup(o => o.Measure1(It.Is<int>(v => 0 < v && v < 10))).Returns("near");
targetMock.Setup(o => o.Measure1(5)).Returns("matched");
// テスト
Assert.Equal("far", target.Measure1(100));
Assert.Equal("near", target.Measure1(4));
Assert.Equal("near", target.Measure1(6));
Assert.Equal("matched", target.Measure1(5));

// 引数に応じた値を返却するモック(2)
// モック対象: public string Measure2(int str)
targetMock
    .Setup(o => o.Measure2(It.IsAny<int>()))
    .Returns((int v) =>
    {
        if (v == 5) return "matched";
        else if (0 < v && v < 10) return "near";
        else return "far";
    });
// テスト
Assert.Equal("far", target.Measure2(100));
Assert.Equal("near", target.Measure2(4));
Assert.Equal("near", target.Measure2(6));
Assert.Equal("matched", target.Measure2(5));

例外をスローするモック

Throws()を使用して例外をスローできます。
モック対象が非同期処理の場合、ThrowsAsync()を使用しますが、各所でasync, awaitの指定が必要になります。(ThrowsAsync()の引数となるラムダ式にasync、各メソッドの呼び出しにawaitの宣言が必要になります。)

var targetMock = new Mock<ITarget>();
ITarget target = targetMock.Object;

// 引数なし例外をスローするモック
// モック対象: public void Validate(string arg)
targetMock
    .Setup(o => o.Validate("ng"))
    .Throws<NullReferenceException>();
// テスト
var ex1 = Assert.Throws<NullReferenceException>(() => target.Validate("ng"));
Assert.Equal("Object reference not set to an instance of an object.", ex1.Message);

// 引数あり例外をスローするモック
// モック対象: public void Validate(string arg)
targetMock
    .Setup(o => o.Validate(null))
    .Throws(new ArgumentNullException("arg"));
// テスト
var ex2 = Assert.Throws<ArgumentNullException>(() => target.Validate(null));
Assert.StartsWith("Value cannot be null.", ex2.Message);

// 引数なし例外をスローするモック(非同期)
// モック対象: public Task ValidateAsync(string arg)
targetMock
    .Setup(o => o.ValidateAsync("ng"))
    .Throws<NullReferenceException>();
// テスト
var ex3 = await Assert.ThrowsAsync<NullReferenceException>(
    async () => await target.ValidateAsync("ng"));
Assert.StartsWith("Object reference not set to an instance of an object.", ex3.Message);

実行回数に基づいた値を返却するモック

SetupSequence()を使用することで、実行回数に基づいた値を返却できます。

var targetMock = new Mock<ITarget>();
ITarget target = targetMock.Object;

// 実行回数に応じて異なる値を返却するモック
// モック対象: public int Increment()
targetMock
    .SetupSequence(o => o.Increment())
    .Returns(1)
    .Returns(2)
    .Returns(3)
    .Throws<InvalidOperationException>();
// テスト
Assert.Equal(1, target.Increment());
Assert.Equal(2, target.Increment());
Assert.Equal(3, target.Increment());
Assert.Throws<InvalidOperationException>(() => target.Increment());

外部条件に基づいた値を返却するモック

When()を使用すると、引数ではなくテストコード上で定義した条件でモックの動作を変更できます。

var targetMock = new Mock<ITarget>();
ITarget target = targetMock.Object;

// 同一引数で異なる値を返却するモック
// (引数以外の外部条件で返却する値を変更するモック)
// モック対象: public string GetDate()
string region = null;
targetMock.When(() => region == "jp").Setup(o => o.GetDate()).Returns("1月1日");
targetMock.When(() => region != "jp").Setup(o => o.GetDate()).Returns("1-1");
// テスト
region = "jp"; Assert.Equal("1月1日", target.GetDate());
region = "en"; Assert.Equal("1-1", target.GetDate());

戻り値なしモックとその検証

戻り値がないメソッドの場合、Verify()で実行有無や実行回数の検証ができます。

var targetMock = new Mock<ITarget>();
ITarget target = targetMock.Object;

// 戻り値なしモックとその検証(1)
// モック対象: public void TestAction1()
targetMock.Setup(o => o.TestAction1());
// テスト(呼び出し回数)
targetMock.Object.TestAction1();
targetMock.Verify(o => o.TestAction1(), Times.Once);

// 戻り値なしモックとその検証(2)
// モック対象: public void TestAction2(string arg)
string innerResult = null;
targetMock
    .Setup(o => o.TestAction2(It.IsAny<string>()))
    .Callback((string arg) => innerResult = $"arg: {arg}");
// テスト
targetMock.Object.TestAction2("arg1");
Assert.Equal("arg: arg1", innerResult);

Callback()を使用することで、より複雑な検証を行うことができます。

メソッドが複数回実行された際の引数を検証する例を次に示します。
テストコード上で引数を格納するリストを宣言し、Callback()メソッドで実行時の引数をリストに追加しています。

// テスト対象クラス
// DIされたIMailServiceを使って2回メッセージを送信する。
public class MessageNotifier
{
    private readonly IMailService _mailService;
    public MessageNotifier(IMailService mailService) => _mailService = mailService;
    public void SendAllMessage()
    {
        _mailService.SendMessage("message1");
        _mailService.SendMessage("message2");
    }
}
// モック対象
public interface IMailService
{
    public bool SendMessage(string message);
}
// モック呼び出し時の引数を保持
var sentMessages = new List<string>();

// テスト対象メソッドとメソッド内部で使用するモックの生成
var mailServiceMock = new Mock<IMailService>();
// モック対象: public bool SendMessage(string message)
mailServiceMock
    .Setup(o => o.SendMessage(It.IsAny<string>()))
    .Callback((string message) => sentMessages.Add(message))
    .Returns(true);

// テスト
var messageNotifier = new MessageNotifier(mailServiceMock.Object);
messageNotifier.SendAllMessage();
Assert.Equal(2, sentMessages.Count);
Assert.Equal("message1", sentMessages[0]);
Assert.Equal("message2", sentMessages[1]);






-.NET Core, 1. システムエンジニアリング, 実装技術

関連記事

おすすめキーボード: MX Keys(KX800)レビュー

キーが浅く指の移動が最小限で抑えられ、多少打鍵位置がずれても正確に押せるので、入力がとても楽で、肩こりや首の痛みが軽減されました。また、在宅勤務用PCや副業用PC等の複数PCの切り替えをキーボードから …

ftp, ftps, sftpの違い

開発対象システムの連携先システムとして、ftpsやらftpsサーバが指定される場合がある。 私の場合、開発標準の役割を担う場合が多く、これらの仕様を把握し、動作確認や単体テスト用のダミーのサーバを用意 …

mavenマルチモジュールプロジェクトの構成例

システム開発でよく使用するmavenマルチモジュールプロジェクトの構成サンプルを説明します。 構成方針 複数のサブシステムをもつシステム開発を想定しています。システム名はzzz、サブシステムはf10, …

LinuxにWildfly18のインストール

Linux(REHL8.3)にWildfly18をインストールする手順を説明します。 概要 RHEL8.3環境を使ったWildfly18+JDK11のインストール手順を説明します。 Red Hat J …

JavaEE7のJSF, Facelets, JSPの関係

JavaEEを使ったアプリ開発の際に、いつも気になるが後回しにしていたこと… HTML5への対応方法の調査等、今後の理解促進のために、調べてみた。 FaceletsとJSFとの関係は? J …