Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
9
Help us understand the problem. What are the problem?

[.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 クラスを用意する必要はありません。

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

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 版は こちら をご覧ください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
9
Help us understand the problem. What are the problem?