Help us understand the problem. What is going on with this article?

TDD を実施するために Moq を試してみた  その1基礎編

C# で頻繁に出てくる Mock Framework の Moq を試してみた。どうやら、先日試していた Autfac とも連携するようだ。Moq, Autofac がある程度理解できて、Linq が書けるようになれば、C# で、そこそこ Test Driven Development ができるかもしれない。

最初にお試しのために、こんなインターフェイスやクラスを書いてみた。

using System;
using Moq;

    public interface IProudct
    {
        string GetContent(string url);
    }

これに対して、Mock のプログラムを書いて試してみる。

基本的な Mock

見ての通り、インターフェイスしかないが、インターフェイスに対して Mock を自動生成できる。テスト駆動開発をする場合は、単体テスト時に外部インターフェイスなどは、Mock したいところだが、自分で MockObject を作るのは面倒くさい。Moq があれば、自動的にやってくれる。

// Interface mocking
var productMock = new Mock<IProudct>();
productMock.Setup(product => product.GetContent("http://www.yahoo.co.jp")).Returns("This is html.");
Console.WriteLine("#[MoqBasic] http://www.yahoo.co.jp:" + productMock.Object.GetContent("http://www.yahoo.co.jp"));
Console.WriteLine("#[MoqBasic] http://www.foo.bar:" + productMock.Object.GetContent("http://www.foo.bar"));

しかもとてもいいことに、入力値と戻り値を書いておけば、その通りにモックしてくる。実際に Moq を使う場合は、productMock.Object.GetContent のように、一旦 Object メソッド経由して、モックを取得して使う。

productMock.Setup(product => product.GetContent("http://www.yahoo.co.jp")).Returns("This is html.");

実際にクラスを実行してみよう。Mockした通りの値が出力されている。Setup メソッドでセットしていない引数で実行した場合は、空で帰ってくるようだ。

#[MoqBasic] http://www.yahoo.co.jp:This is html.
#[MoqBasic] http://www.foo.bar:

クラスの Mock

では、インターフェイスに対する Mock はできたが、クラスに対してはどうだろうか?

    public class ProductImpl : IProudct
    {
        public virtual string GetContent(string url) // virtual が必要
        {
            return $"{url}: <html><head><title>hello</title></head><body>Hello!</body></html> "; 
        }
    }

モックのSetup は全く同様だ。

// Class mocking

var productImplMock = new Mock<ProductImpl>();
productImplMock.Setup(product => product.GetContent("http://www.yahoo.co.jp")).Returns("This is html.");
Console.WriteLine("#[MoqBasic] http://www.yahoo.co.jp:" + productImplMock.Object.GetContent("http://www.yahoo.co.jp"));
Console.WriteLine("#[MoqBasic] http://www.foo.bar:" + productImplMock.Object.GetContent("http://www.foo.bar"));

実行結果は次の通り

#[MoqBasic] http://www.yahoo.co.jp:This is html.
#[MoqBasic] http://www.foo.bar:

全く同じだ。Setup で指定しなければ、元のメソッドが呼ばれるわけではないのに注意。また、一点だけ注意点があり、クラスを Mock したい場合は、メソッドに virtualキーワードが必要だ。これを無くすと

System.NotSupportedException: 'Invalid setup on a non-virtual (overridable in VB) member: product => product.GetContent("http://www.yahoo.co.jp")'

という感じのエラーが出る。Mock で、メソッドをオーバーライドするのに、virtual がついていないと override 出来ないからだ。

ネストしたオブジェクトの Mock

ちなみに、クラスがさらに、他のクラスを持っているようなケースがあるが、その場合も、ネストしたオブジェクト毎 Mock してくれる。先ほどのプログラムを改造してみよう。

    public class ProductType
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
    }
    public interface IProudct
    {
        string GetContent(string url);
        ProductType ProductType { get; set; }

    }

Mock 部

var productMock = new Mock<IProudct>();

// Nested Object Mocking
productMock.Setup(product => product.ProductType.Name).Returns("Suppliment");
Console.WriteLine("#[MoqBasic] IProduct.ProductType.Name:" + productMock.Object.ProductType.Name);

実行結果は

#[MoqBasic] IProduct.ProductType.Name:Suppliment

というわけで、ネストしたオブジェクトもむっちゃ簡単に Mock できる!これはいいね!

Mock メソッドが適切な値で呼ばれたかの確認

TDD をやっていて、Mock を使う場面では、偽物のオブジェクトを作るだけじゃなくて、その偽物オブジェクトに、適切な値がわかってきたか?を確かめたい場合がある。そのために verify というメソッドが用意されているようだ。

// Validate the parameter
try
{
    productMock.Verify(product => product.GetContent("http://www.yahoo.co.jp"));
    productMock.Verify(product => product.GetContent("http://www.microsoft.com"));
} catch (MockException ex)
{
    Console.WriteLine("#[MoqBasic]" + ex.Message);
}

こんな感じで書いておくと、結果は次の通り。基本的に、予想した値が Mock にわたっていなければ例外が発生する。

Expected invocation on the mock at least once, but was never performed: product => product.GetContent("http://www.microsoft.com")

Configured setups:
product => product.GetContent("http://www.yahoo.co.jp")

Performed invocations:
IProudct.GetContent("http://www.yahoo.co.jp")
IProudct.GetContent("http://www.foo.bar")
IProudct.ProductType

最低限だったが、この最低限のセットがあれば、既にテスト駆動を始められるだろう。他にも、CallBack や Event さらに Linq に対しても Mock がかけられるらしいので、次回以降で試していきたい。

今回の動作するコードはこちらに置いておいたので参考にしてください。

TsuyoshiUshio@github
プログラマ。自分の学習用のブログです。内容は会社とは一切関係ありません。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away