Moqでオーバーライドできないメソッドをモックする

2023-03-18

最近C#でAzure Functionsのユニットテストを書こうと検討しているが、 ある外部ライブラリのクラスに依存するクラスはモックを使うことで外部ライブラリを使用するために必要な準備を避け、 できるだけ自分が実装したコードのみのテストをしたい。

テストしたいコードは下記のHelloWorldRunAsyncという関数。
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はモック対象のクラスを継承したクラスを作り、モックできるようにメソッドをオーバーライドしているようなのだが、 NumberGeneratorGetNumbervirtualがついていないためオーバーライドできずエラーになっているようだ。

System.NotSupportedException : Unsupported expression: klass => klass.GetNumber()
Non-overridable members (here: NumberGenerator.GetNumber) may not be used in setup / verification expressions.

解決するためには単純にGetNumbervirtualをつけてオーバーライド可能にすればいいのだが、今回は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);
        // 以下略
    }

本来はライブラリの実装者がインターフェースを公開していれば今回のケースは避けられるが、 どうしてもオーバーライドできないメソッドをモックしたいケースはラッパークラスを作ることで対処できる。