見出し画像

[.NET] 単体テストがさくっと書ける!モック化の枠組み(Moq + Autofac)

モックライブラリ Moq とIoCコンテナ Autofac を使用してモック化する例です。
単体テストの基底クラスにモックの生成、検証を抽出することで、Verify 漏れを防ぎ、素早く、すっきりテストコードを書くことができます。

■プロダクションコード

Autofac モジュール

ここでは、既定で protected プロパティに注入するように構成します。

internal class ProtectedPropertyInjectionModule : Autofac.Module
{
   private static readonly DelegatePropertySelector ProtectedSetterSelector =
       new DelegatePropertySelector((p, o) => p.CanWrite && (p.SetMethod?.IsFamily ?? false));

   protected override void AttachToComponentRegistration(
       IComponentRegistryBuilder componentRegistry,
       IComponentRegistration registration)
   {
       registration.PipelineBuilding += (sender, pipeline) =>
       {
           pipeline.Use(PipelinePhase.Activation, MiddlewareInsertionMode.EndOfPhase, (c, next) =>
           {
               next(c);

               c.InjectProperties(c.Instance, ProtectedSetterSelector);
           });
       };
   }
}

アプリケーション開始処理

DIを構成し、アプリケーションを開始します。
※フレームワークに応じた適切な箇所に記述してください。

public static void StartApplication()
{
   var builder = new ContainerBuilder();

   builder.RegisterModule(new ProtectedPropertyInjectionModule());
   builder.RegisterType<Service>().SingleInstance();
   builder.RegisterType<Client>();

   var container = builder.Build();

   var client = container.Resolve<Client>();
   var result = client.Act();

   Console.WriteLine(result);
   // Production
}

クライアントクラス(System Under Test)

public class Client
{
   /// <summary>
   /// 依存サービス。
   /// </summary>
   protected Service Service { get; set; }

   /// <summary>
   /// テスト対象メソッド。
   /// </summary>
   public string Act()
   {
       // プロパティ経由でDIコンテナから依存オブジェクトを取得して使用する。
       return this.Service.GetText();
   }
}

依存サービス(Depended-On Component)

public class Service
{
   private readonly string text = "Production";

   public virtual string GetText()
   {
       return this.text;
   }
}

■単体テスト

テストの基底クラス

DIコンテナの管理、モックオブジェクトの生成、管理、検証(Verify)を担います。

(MSTest 例)

/// <summary>
/// 単体テストの基底クラス。
/// </summary>
public abstract class AutofacTestBase
{
   delegate void BuilderAction(ContainerBuilder builder);

   private BuilderAction registerMocks;

   private List<Mock> mocks;

   /// <summary>
   /// テストの初期処理。
   /// </summary>
   [TestInitialize]
   public void BaseTestInitialize()
   {
       this.mocks = new List<Mock>();
   }

   /// <summary>
   /// テストの終了処理。
   /// </summary>
   [TestCleanup]
   public void BaseTestCleanup()
   {
       // モックが期待どおりに呼び出されたことを検証する。
       foreach (var mock in this.mocks)
       {
           mock.VerifyAll();
       }
   }

   /// <summary>
   /// Mock インスタンスを生成する。
   /// </summary>
   protected Mock<T> CreateMock<T>()
       where T : class
   {
       return CreateMock<T>(false);
   }

   /// <summary>
   /// Mock インスタンスを生成する。
   /// </summary>
   protected Mock<T> CreateMock<T>(bool callBase)
       where T : class
   {
       var mock = new Mock<T> { CallBase = callBase };
       this.mocks.Add(mock);
       
       // RegisterMock はジェネリック引数(ここでは暗黙指定)から型を判定するので、登録処理自体をデリゲートに収める。
       // (List<Mock> では型を渡せない)
       this.registerMocks += builder => builder.RegisterMock(mock);
       
       return mock;
   }

   /// <summary>
   /// AutoMock オブジェクトを取得する。
   /// </summary>
   protected AutoMock GetLoose()
   {
       return AutoMock.GetLoose(builder =>
       {
           builder.RegisterModule(new ProtectedPropertyInjectionModule());
           this.registerMocks.Invoke(builder);
       });
   }
}

テストクラス

モックをDIコンテナに渡すので、依存コンポーネントを差し替えるためだけに Client を継承した Testable クラスを用意する必要はありません。

モックインスタンスは、個々のテストメソッドに必要なものだけ生成します。

[TestClass]
public class ClientTest : AutofacTestBase
{
   [TestMethod]
   public void Act_Mocking()
   {
       // モック設定
       var serviceMock = CreateMock<Service>();
       serviceMock.Setup(s => s.GetText()).Returns("UnitTest");

       // モックのDI登録
       using (var autoMock = GetLoose())
       {
           // テスト対象オブジェクトを生成
           var client = autoMock.Create<Client>();

           // テスト対象メソッドを実行
           string result = client.Act();

           // 戻り値の検証
           // ※モックの検証は基底クラスで自動的に行われる。
           Assert.AreEqual("UnitTest", result);
       }
   }
}

---

※掲載コードはサンプルですので、実際のプロジェクトでは、細かい点を調整してご活用ください。

※Unity 版は こちら をご覧ください。

この記事が気に入ったらサポートをしてみませんか?