最近C#でAzure Functionsのユニットテストを書こうと検討しているが、 ある外部ライブラリのクラスに依存するクラスはモックを使うことで外部ライブラリを使用するために必要な準備を避け、 できるだけ自分が実装したコードのみのテストをしたい。
テストしたいコードは下記のHelloWorld
のRunAsync
という関数。
HTTPリクエストを受けるとNumberGenerator
クラスのGetNumber
という数値を返すのメソッドを呼んで、受け取った値をレスポンスとして返す。
今回NumberGenerator
が外部ライブラリのクラスであるという想定で、HelloWorld
クラスのコンストラクタでNumberGenerator
をDependency Injection (DI)するようにしている。
public class HelloWorld
{
private readonly NumberGenerator _klass;
public HelloWorld(NumberGenerator klass)
{
_klass = klass;
}
[FunctionName("HelloWorld")]
public async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
{
var num = _klass.GetNumber();
return new OkObjectResult($"{num}");
}
}
NumberGeneratorは下記の通り。
public class NumberGenerator
{
private int Number { get; set; }
public NumberGenerator(int number)
{
Number = number;
}
public int GetNumber()
{
return Number;
}
}
さて、ここでNumberGeneratorをモックして、HelloWorldクラスのRunAsync関数をテストすることを考える。 モックにはMoqというライブラリを用いてGetNumberが2を返すようにモックする。
public class Tests
{
[Test]
public async Task Test1()
{
var mockKlass = new Mock<NumberGenerator>(MockBehavior.Strict, 10);
mockKlass.Setup(klass => klass.GetNumber()).Returns(2);
var myFunction = new HelloWorld(mockKlass.Object);
var res = (OkObjectResult)await myFunction.RunAsync(new DefaultHttpRequest(new DefaultHttpContext()));
Assert.AreEqual("2", res.Value);
}
}
テストを実行すると下記のようにオーバーライドできないメンバはセットアップできないと言われる。
Moqはモック対象のクラスを継承したクラスを作り、モックできるようにメソッドをオーバーライドしているようなのだが、
NumberGenerator
のGetNumber
はvirtual
がついていないためオーバーライドできずエラーになっているようだ。
System.NotSupportedException : Unsupported expression: klass => klass.GetNumber()
Non-overridable members (here: NumberGenerator.GetNumber) may not be used in setup / verification expressions.
解決するためには単純にGetNumber
にvirtual
をつけてオーバーライド可能にすればいいのだが、今回はNumberGenerator
が外部ライブラリのため変更できない制約があるケースを考える。
結論をいうとオーバーライドできるようにオーバライドできないクラスのラッパー関数を作ればよい。
public class NumberGeneratorWrapper : NumberGenerator
{
public NumberGeneratorWrapper(int number) : base(number) {}
public virtual int GetNumber()
{
return 2;
}
}
そしてHelloWorld
クラスではラッパークラスを参照するようにする。
public class HelloWorld
{
private readonly NumberGeneratorWrapper _klass;
public HelloWorld(NumberGeneratorWrapper klass)
{
_klass = klass;
}
// 以下略
}
テストコードもラッパークラスを参照するようにする。
public async Task Test1()
{
var mockKlass = new Mock<NumberGeneratorWrapper>(MockBehavior.Strict, 10);
mockKlass.Setup(klass => klass.GetNumber()).Returns(2);
// 以下略
}
本来はライブラリの実装者がインターフェースを公開していれば今回のケースは避けられるが、 どうしてもオーバーライドできないメソッドをモックしたいケースはラッパークラスを作ることで対処できる。