MoqとNSubstitute:シンタックスチートシート

ユニットテストを書くときには、依存関係をモックにしたくなることがよくあります。この方法で依存関係の振る舞いを定義し、テスト対象システムの完全なコントロールを取れます。

.NETアプリケーションにおいては、もっともよく使われているモックライブラリにはMoqNSubstituteがあります。これらはクラスに注入されたサービスの振る舞いを作成・カスタマイズすることができます。似ている機能を持っているものの、シンタックスに少しだけ違いがあります。

この記事では、これら2つのライブラリがよく使われている機能をどう実装しているのかを学びます。そのために必要であれば、片方からもう片方に簡単に移行できるようになります。

実際例


いつものように、実際の例を使って説明しましょう。

この記事のために、StringsWorkerというダミーのクラスを作成しました。これは他のサービスIStringUtilityを呼び出すことしかしていません。

public class StringsWorker
{
    private readonly IStringUtility _stringUtility;

    public StringsWorker(IStringUtility stringUtility)
        => _stringUtility = stringUtility;

    public string[] TransformArray(string[] items)
        => _stringUtility.TransformAll(items);

    public string[] TransformSingleItems(string[] items)
        => items.Select(i => _stringUtility.Transform(i)).ToArray();

    public string TransformString(string originalString)
        => _stringUtility.Transform(originalString);
}

StringsWorkerクラスをテストするために、唯一の依存関係であるIStringUtilityをモックにします。つまり、IStringUtilityを実装する具体的なクラスではなく、MoqとNSubstituteを使用してモックし、その振る舞いを定義して実際のメソッド呼び出しをシミュレートします。

もちろん、この2つのライブラリを使うには各テストプロジェクトにインストールする必要があります。

モック化された依存関係の定義


最初にやることは、新しいモックのインスタンスを作成することです。

MoqではMock<IStringUtility>の新しいインスタンスを生成し、それのObjectプロパティをStringsWorkerのコンストラクタに注入します。

private Mock<IStringUtility> moqMock;
private StringsWorker sut;

public MoqTests()
{
    moqMock = new Mock<IStringUtility>();
    sut = new StringsWorker(moqMock.Object);
}

NSubstituteでは、「Substitute.For<IStringUtility>()」で宣言します。これはIStringUtilityを返しますが、いかなるクラスにもラップされていません。そして、それをStringsWorkerのコンストラクタに注入します。

private IStringUtility nSubsMock;
private StringsWorker sut;

public NSubstituteTests()
{
    nSubsMock = Substitute.For<IStringUtility>();
    sut = new StringsWorker(nSubsMock);
}

これで、moqMocknSubsMockの振る舞いをカスタマイズして、それらの依存関係への呼び出しを確認できます。

特定の入力値に対するメソッドの結果を定義する:Return()メソッド


例えば、Transformメソッドに「"ciao"」というパラメータを渡すたびに、それが「"hello"」を返すように依存関係をカスタマイズしたいとします。

MoqでSetupReturnsの組み合わせを使用します。

moqMock.Setup(_ => _.Transform("ciao")).Returns("hello");

NSubstituteではSetupを使用せず、直接Returnsを呼び出します。

nSubsMock.Transform("ciao").Returns("hello");

入力値にかかわらずのメソッドの結果を定義する:It.IsAny() vs Arg.Any()


今度はTransformメソッドに渡された実際の値は関係なく、その値が何であれメソッドは常に「"hello"」を返すようにしたいとします。

MoqではIt.IsAny<T>()を使ってTのタイプを指定します。

moqMock.Setup(_ => _.Transform(It.IsAny<string>())).Returns("hello");

NSubstituteではArg.Any<T>()を使います。

nSubsMock.Transform(Arg.Any<string>()).Returns("hello");

入力にフィルタをかけてメソッドの結果を定義する:It.Is() vs Arg.Is()


入力パラメータに条件が満たされた場合だけ特定の結果を返したいとしましょう。

例えば、Transformメソッドに「"IT"」で始まる文字列を渡すたびに、それが「"ciao"」を返す必要があります。

MoqではIt.Is<T>(func)を使用し、入力として式を渡します。

moqMock.Setup(_ => _.Transform(It.Is<string>(s => s.StartsWith("IT")))).Returns("ciao");

同様に、NSubstituteでは、「Arg.Is<T>(func)」を使用します。

nSubsMock.Transform(Arg.Is<string>(s => s.StartsWith("IT"))).Returns("ciao");

ちょっとした豆知識ですが、NSubstituteにおいてフィルタは「Expression<Predicate<T>>」型で、Moqでは「Expression<Func<TValue, bool>>」型ですが、同じように書くことができますよ!

例外を投げる


ハッピーなパスだけでなく、エラーが発生する場合もテストするべきなので、注入されたサービスが例外を投げるテストを書くべきですし、その例外が適切に処理されているかを確認する必要があります。

どちらのライブラリでも、そのタイプを指定することによって、一般的な例外を投げることができます。

//Moq
moqMock.Setup(_ => _.TransformAll(null)).Throws<ArgumentException>();

//NSubstitute
nSubsMock.TransformAll(null).Throws<ArgumentException>();

特定の例外インスタンスを投げることもできます - エラーメッセージを追加したい場合など。

var myException = new ArgumentException("My message");

//Moq
moqMock.Setup(_ => _.TransformAll(null)).Throws(myException);

//NSubstitute
nSubsMock.TransformAll(null).Throws(myException);

その例外を処理しないで、上に伝播させたい場合は、次のように確認することができます。

Assert.Throws<ArgumentException>(() => sut.TransformArray(null));

受け取った呼び出しを検証する:Verify() vs Received()


コードが期待される実行パスに沿っているかを理解するために、あるメソッドが特定のパラメータで呼び出されているかどうかを検証したい場合があります。

これを検証するには、MoqではVerifyメソッドを使います。

moqMock.Verify(_ => _.Transform("hello"));

NSubstituteを使う場合Receivedメソッドを使います。

nSubsMock.Received().Transform("hello");

以前に見たように、It.IsAnyIt.IsArg.AnyおよびArg.Isを使用して、入力として渡されるパラメータのいくつかの特性を検証することができます。

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/bellonedavide/moq-vs-nsubstitute-syntax-cheat-sheet-kkf