DI利用時のUnitTestの課題を解消する:Mock Injection Factoryパターン

Dependency Injectionパターンを用いたクラスのテストコードを記述しているとき、Injectionする対象が増えた際、修正が広範に及んでしまう事があります。

このとき、依存オブジェクトを注入して生成するFactoryクラスを利用してテスト対象のクラスを生成することで解決ができます。

背景

次のClientのようなテスト対象のクラスが存在したとします。

public class Client
{
    private IFooServer FooServer { get; }

    public Client(IFooServer fooServer)
    {
        FooServer = fooServer;
    }

    public void Foo()
    {
        FooServer.FooService();
    }
}

public interface IFooServer
{
    void FooService();
}

ClientはFooメソッドが呼び出されると、コンストラクタで渡されていた(コンストラクタ インジェクション)IFooServerオブジェクトのFooServiceメソッドに処理を移譲しています。

このClientのFooメソッドにたいしてテストを実施するとき、IFooServiceはテスト対象とせずMockを利用してテストするとした場合、テストコードは次のようになります。

public class FooServerMock : IFooServer
{
    public void Foo()
    {
        ...
    }
}

public void TestFoo()
{
    var client = new Client(new FooServerMock());
    client.Foo();
    ...
}

ここでClientクラスにBarメソッドを追加し、その実装のためにIBarServerのBarServiceメソッドを呼び出す必要があったとします。Clientクラスをつぎのように修正する必要があります。

public class Client
{
    private IFooServer FooServer { get; }
    private IBarServer BarServer { get; }

    public Client(IFooServer fooServer, IBarServer barServer)
    {
        FooServer = fooServer;
        BarServer = barServer;
    }

    public void Bar()
    {
        BarServer.BarService();
    }

    ...
}

さて、このような修正が入った場合、新たなメソッドに対するテストコードを記述するだけでは済みません。先に記載されたテストコードも修正する必要があります。

public void TestFoo()
{
    //var client = new Client(new FooServerMock());
    var client = new Client(new FooServerMock(), new BarServerMock());
    client.Foo();
}

プロダクションコード側は依存性を自動的に解決してくれるDependency Injection Containerを利用していれば問題ありません。

しかしテストコード側は、テストケースにあわせて任意のMockを注入する必要があります。そのためテストケースが多ければ修正箇所は爆発的に増加します。またコードの変更履歴的にも、本質的に影響がない箇所が変更されることは好ましくありません。

解法

テスト対象のインスタンスをFactoryクラスを通して生成することで解決が可能です。

public class ClientFactory
{
    public IFooServer FooServer { get; set; } = new NotImplementedFooServer();

    public Client Create()
    {
        return new Client(FooServer);
    }

    private class NotImplementedFooServer : IFooServer
    {
        public void FooService()
        {
            throw new System.NotImplementedException();
        }
    }
}

Factoryクラスは公開プロパティに注入対象のオブジェクトを持ち、デフォルトのMockを初期値としてもちます。公開プロパティは任意で更新可能とします。このFactoryをつぎのように利用します。

public class FooServerMock : IFooServer
{
    public void FooService()
    {
    }
}

public void TestFoo()
{
    var factory = new ClientFactory{ FooServer = new FooServerMock() };
    var client = factory.Create();
    client.Foo();
}

あらたにClientのコンストラクタにIFooServerを注入する必要が発生した場合、Factoryをつぎのように修正します。

public class ClientFactory
{
    public IFooServer FooServer { get; set; } = new NotImplementedFooServer();
    public IBarServer BarServer { get; set; } = new NotImplementedBarServer();

    public Client Create()
    {
        return new Client(FooServer, BarServer);
    }

    private class NotImplementedBarServer : IBarServer
    {
        public void BarService()
        {
            throw new System.NotImplementedException();
        }
    }

    ...
}

既存のテストコードへの影響は一切ありません。

このようにMockを公開プロパティにもつテスト対象のFactoryクラスを利用することでDependency Injectionパターンを利用した際のひとつのデメリットを解消することができます。

with Moq

.NETにはMockフレームワークとしてMoqという優れたライブラリが存在します。本パターンをアレンジして適用することでMoqをスマートに利用することができます。

Factoryをつぎのように実装します。

public class ClientFactory
{
    public Mock<IFooServer> FooServer { get; set; } = new Mock<IFooServer>();

    public Client Create()
    {
        return new Client(FooServer.Object);
    }
}

IFooServerではなく、そのMockのベースとなるMock<IFooServer>を保持することでテストコードをスマートに記述することができます。

例えばClientのFooメソッドが呼び出されたときに、IFooServerのFooServiceが一度だけ呼び出されていることを評価する場合はつぎのように記述します。

public void TestFoo()
{
    var factory = new ClientFactory();
    var client = factory.Create();
    client.Foo();

    factory
        .FooServer
        .Verify(x => x.FooService(), Times.Once);
}

最後に

本パターンは筆者の経験則から独自に検討したものです(類似のパターンはあるでしょう)。もっとスマートな解決策があるのであれば、どしどし連絡をお待ちしています。

この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

3
Xamarin / WPF / C# / .NETなど。私の発言は私のものです。 Microsoft MVP for Development Technologies(2017/01〜)。
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。