はじめに
- 次の環境を使用して動作確認しています。
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の使い方はこちらで紹介しています。
- Moq関連の情報
基本的な使用方法
基本的なモック・スタブ
基本的なモック・スタブの作成方法を次に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // モックオブジェクトを生成 var targetMock = new Mock<ITarget>(); ITarget target = targetMock.Object; // モック化対象のオブジェクトを直接取得も可 //ITarget target = Mock.Of<ITarget>(); // 特定引数に対して固定値を返却するモック // モック対象: 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.Of<T>()を使用して、モック化対象オブジェクトを直接取得することもできます。 - MockオブジェクトのSetup系メソッドでプロパティやメソッド呼び出しの条件を指定します。
条件として任意の引数を指定する場合は、It.Any<型>()を指定します。 - Returns系メソッドで戻り値を指定します。
- 実行時にモックとして指定したプロパティやメソッドの引数と合致しない場合、戻り値の型に応じた既定値が返却されます。(int型なら0、string型ならnullなど)
- テストで使用する実際のモック化されたオブジェクトは、MockオブジェクトのObjectプロパティから取得します。
引数に基づいた値を返却するスタブ
複数のSetupの定義やラムダ式を使用して、引数に基づいた値を返却できます。
次の例では、Measure1(), Measure2()ともに、引数に基づいて”matched”, “near”, “far”のいずれかを返却します。Measure1()は複数のSetup()で、Measure2()はラムダ式で、同一条件を定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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の宣言が必要になります。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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()を使用することで、実行回数に基づいた値を返却できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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()); |
プロパティのスタブ
SetupProperty()を使用すると、設定値を保持可能(追跡可能)なプロパティを実装できます。
個別の指定が面倒な場合、SetupAllProperties()で全てのプロパティのスタブを一括で作成できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var targetMock = new Mock<ITarget>(); ITarget target = targetMock.Object; // 値を保持可能なプロパティの実装 // ("o = > o.Data1.Data2.Value"等のようにネストしたプロパティの定義も可) targetMock.SetupProperty(o => o.StrProp); // Get+Set実装 targetMock.SetupProperty(o => o.IntProp, 1); // Get+Set実装、初期値設定 // テスト Assert.Null(target.StrProp); target.StrProp = "test"; Assert.Equal("test", target.StrProp); // テスト Assert.Equal(1, target.IntProp); target.IntProp = 2; Assert.Equal(2, target.IntProp); |
読み取り専用プロパティ(getのみでsetプロパティアクセサーがないプロパティ)のスタブを作成する場合は、Setup()を使用します。
SetupGet()でも同様に実現できますが、このメソッドは後述のように検証目的のものです。混乱を避けるために、読み取り専用プロパティを実現する手段として使用しない方が良いと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 | var targetMock = new Mock<ITarget>(); ITarget target = targetMock.Object; // setがないプロパティにSetupProperty()するとArgumentException // ("Property ITarget.ReadonlyStrProp does not have a setter.") // targetMock.SetupProperty(o => o.ReadonlyStrProp, "test"); targetMock.Setup(o => o.ReadonlyStrProp).Returns("test"); // 例1 targetMock.SetupGet(o => o.ReadonlyIntProp).Returns(1); // 例2 // テスト Assert.Equal("test", target.ReadonlyStrProp); Assert.Equal(1, target.ReadonlyIntProp); |
外部条件に基づいた値を返却するモック・スタブ
When()を使用すると、引数ではなくテストコード上で定義した条件でスタブの動作を変更できます。
1 2 3 4 5 6 7 8 9 10 11 12 | 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()); |
void型のスタブメソッドとその検証
戻り値がないメソッドの場合、Verify()で実行有無や実行回数の検証ができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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()メソッドで実行時の引数をリストに追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // テスト対象クラス // 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // モック呼び出し時の引数を保持 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]); |