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 がかけられるらしいので、次回以降で試していきたい。
今回の動作するコードはこちらに置いておいたので参考にしてください。