読者です 読者をやめる 読者になる 読者になる

複数のインタフェースを持つモックを Moq で作る

ユニットテストでモックを簡単に作るためのライブラリの有名どころの一つに Moq があります。

基本的な使い方の例としてはこんな感じです。

public interface IFoo
{
  bool IsFoo(int value);
}

public class Foo : IFoo
{
  public bool IsFoo(int value)
  {
    // do something
  }
}

public class Fuga
{
  public IFoo Foo { get; set; }
  public string Fuga(int value)
  {
    return Foo.IsFoo(value) ? "expected" : "";
  }
}

[TestCase("expected")]
public void Test(string expected)
{
  var mock = new Mock<IFoo>(); // モックの型を定義
  mock.Setup(_ => _.IsFoo(It.IsAny<int>())).Returns(true); // モックの振る舞いを定義
  var target = new Fuga { Foo = mock.Object }; // モック作成

  target.Fuga().Is(expected);
}

非常に簡単にモックが作れますね。

さてさて。ではテスト対象コードが次のようになっていたらどうでしょうか。

public interface IFoo
{
  bool IsFoo(int value);
}

public interface IBar
{
  bool IsBar(int value);
}

public class BaseClass
{
}

public class FooBar : BaseClass, IFoo, IBar
{
  public bool IsFoo(int value)
  {
    // do something
  }

  public bool IsBar(int value)
  {
    // do something
  }
}

public class Hoge
{
  public BaseClass BaseClass { get; set; }
  public string Fuga(int value)
  {
    var foo = FooBar as IFoo;
    if (foo == null) return "";
    return foo.IsFoo(value) ? "expected" : "";
  }
}

あまりいい例でないですが。。。

Hoge クラスの Fuga メソッドでは、メンバの FooBarIFoo に型変換を試してみて、もし変換可能であればそのインタフェースが持つメソッドを呼び出しています。実際の例として、IDisposable なクラスとそうでないクラスが混在するケースなんかがあるようです。

では再びこのクラスのテストを書いてみましょう。

[TestCase("expected")]
public void Test(string expected)
{
  var mock = new Mock< **FooBar** >(); // おや?
  mock.Setup(_ => _.IsFoo(It.IsAny<int>())).Returns(true);
  var target = new Fuga { BaseClass = mock.Object }; // モック作成

  target.Fuga().Is(expected);
}

すぐに思いつくのはこんな感じですかね。テストを実行してみます。

System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member: _ => _.IsFoo(It.IsAny())

あらら。エラーになってしまいました。  

このエラーは、Moq の仕様です。

大雑把に言うと、モックの作成は内部的には対象を継承したクラスを作り、メソッドの振る舞いを override しているだけです。なので、モック可能なのはインタフェースか継承可能なクラス、さらに振る舞いを上書きするには abstract や virtual である必要があります。今回モックを作成しようとした FooBar クラスの IsFoo メソッドはこの何れにも該当しないためエラーになりました。

ではどうしましょうか。

まぁ FooBar クラスに手を入れられるのであれば、メソッドを virtual にするのが手っ取り早いです。しかし、テストの為にプロダクションコードの設計を変えたくはありません。あるいは、この FooBarサードパーティ製のライブラリだったりするかもしれません。

FooBar と同じ継承元を持つ、TestFooBar クラスをテストの為だけに用意する手もあります。しかしこれはイマイチです。そんなことをやるならわざわざ Moq を持ち出したりしません。

ではどうしましょうか?こうします。

[TestCase("expected")]
public void Test(string expected)
{
  var mock = new Mock<FooBar>();
  mock.As<IFoo>()
      .Setup(_ => _.IsFoo(It.IsAny<int>())).Returns(true);
  var target = new Fuga { BaseClass = mock.Object }; // モック作成

  target.Fuga().Is(expected);
}

内部的には IFoo に型変換しているので、そこだけ差し替えてやればいいのです。
そのために Moq では As メソッドというものがあります。

このような状況は決して多くはないと思いますが、ありえないとも言い切れないので(というかさっき自分がぶつかった)、覚えておくといつか何かの役に立つかもしれません。