モックライブラリ Moq とIoCコンテナ Autofac を使用してモック化する例です。
単体テストの基底クラスにモックの生成、検証を抽出することで、Verify 漏れを防ぎ、素早く、すっきりテストコードを書くことができます。
※単体テストでDIコンテナを使用することの是非はここでは問いません。
■ライブラリ
- Moq
- Autofac (v6)
- Autofac.Extras.Moq
■プロダクションコード
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 クラスを用意する必要はありません。
モックインスタンスは、個々のテストメソッドに必要なものだけ生成します。
MSTest例
[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 版は こちら をご覧ください。