web-dev-qa-db-ja.com

依存関係の逆転によりAPIが拡張され、不要なテストが発生する

この質問は私を数日間悩ませてきました、そしてそれはいくつかの実践が互いに矛盾しているように感じます。

反復1

public class FooDao : IFooDao
{
    private IFooConnection fooConnection;
    private IBarConnection barConnection;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooConnection = fooConnection;
        this.barConnection = barConnection;
    }

    public Foo GetFoo(int id)
    {
        Foo foo = fooConection.get(id);
        Bar bar = barConnection.get(foo);
        foo.bar = bar;
        return foo;
    }
}

これをテストするとき、IFooConnectionとIBarConnectionを偽造し、FooDaoをインスタンス化するときに依存性注入(DI)を使用します。

機能を変更せずに実装を変更できます。

反復2

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

さて、このビルダーは書きませんが、FooDaoが以前に行ったのと同じことを想像してください。これは単なるリファクタリングなので、機能は変更されないので、テストは引き続き成功しています。

IFooBuilderは、ライブラリで機能するためだけに存在するため、内部です。つまり、APIの一部ではありません。

唯一の問題は、依存関係の逆転に準拠しなくなったことです。この問題を修正するためにこれを書き直した場合、次のようになります。

反復

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

これでうまくいくはずです。私はコンストラクタを変更したので、これをサポートするために(またはその逆)、テストを変更する必要がありますが、これは私の問題ではありません。

これをDIと連携させるには、FooBuilderとIFooBuilderをパブリックにする必要があります。これは、FooBuilderが突然ライブラリのAPIの一部になったため、FooBuilderのテストを作成する必要があることを意味します。私の問題は、ライブラリのクライアントは、意図したAPI設計であるIFooDaoを介してのみ使用する必要があり、テストはクライアントとして機能することです。依存関係の逆転に従わない場合、テストとAPIはよりクリーンになります。

つまり、クライアントとして、またはテストとして私が気にしているのは、正しいFooを取得することであり、構築方法ではありません。

ソリューション

  • 私は気にしないでください、DIを喜ばせるためだけに公開されていますが、FooBuilderのテストを記述しますか? -反復3をサポートします

  • APIの拡張は依存関係の逆転の欠点であることを理解し、ここでAPIに準拠しないことを選択した理由を明確にする必要がありますか? -反復2をサポートします

  • クリーンなAPIに重点を置いていますか? -反復3をサポートします

編集:私はそれを明確にしたい、私の問題はnot「内部をテストする方法?」ではなく、「内部に保持でき、それでもDIPに準拠できますか? ?」.

8
Chris Wohlert

私は一緒に行きます:

APIの拡張は依存関係の逆転のマイナス面であることを理解し、ここで準拠しないことを選択した理由を明確にすべきですか? -反復2をサポート

ただし、 (OO and Agile Design)のSOLID原則 (1:08:55のa)で、ボブおじさんは依存関係注入に関する彼のルールは注入しないと言っていますすべて、戦略的な場所でのみ注入します(依存関係の逆転と依存関係の注入のトピックは同じであるとも述べています)。

つまり、依存関係の逆転は、プログラム内のすべてのクラスの依存関係に適用されることを意図したものではありません。あなたは戦略的な場所でそれをするべきです(それが支払う(または支払うかもしれない)とき)。

私が見たさまざまなコードベースのいくつかの経験/観察を共有します。 N.B.私は特にアプローチを推奨していません。そのようなコードが起こっているのを目にしたところで共有するだけです:

私が気にしない場合は、DIを喜ばせるためだけに公開されていますが、FooBuilderのテストを記述します

DIと自動テストの前は、必要な範囲に何かを制限する必要があるという認識が受け入れられていました。しかし最近では、テストを容易にするためにcouldを内部に公開したままにするメソッドが見られることは珍しくありません(これは通常、別のアセンブリで発生するため)。 N.B.内部メソッドを公開するディレクティブがありますが、これは多かれ少なかれ同じことです。

APIの拡張は依存関係の逆転の欠点であることを理解し、ここでAPIに準拠しないことを選択した理由を明確にする必要がありますか?

DIは間違いなく追加のコードによるオーバーヘッドを招きます。

クリーンなAPIに重点を置きすぎていますか

DIフレームワークdoは追加のコードを導入しているため、DIスタイルで記述されている場合、DI自体、つまりSOLIDからのDを使用しないコードへのシフトに気づきました。

要約すれば:

  1. テストを簡略化するためにアクセス修飾子が緩められることがある

  2. DIは追加のコードを導入します

1と2に照らして、一部の開発者は、これはあまりにも多くの犠牲であり、単純に最初は抽象化に依存することを選択し、後でDIをレトロフィットするオプションを選択します。

5
Robbie Dee

適切なテストを実行するためにプライベートメソッドを(なんらかの方法で)公開する必要があるというこの概念を特に購入しません。クライアント向けAPIはnotがオブジェクトのパブリックメソッドと同じであることもお勧めします。これがあなたの公開インターフェースです:

interface IFooDao {
   public Foo GetFoo(int id);
}

そしてあなたのFooBuilderについての言及はありません

元の質問では、FooBuilderを使用して3番目のルートをとります。私はおそらくmockを使用してFooDaoをテストし、FooDao機能に集中します(簡単であれば、いつでも実際のビルダーを注入できます)。 FooBuilderをテストします。

(この質問/回答でモッキングが言及されていないことに驚いています-これはDI/IoCパターンのソリューションのテストと密接に関連しています)

再あなたのコメント:

前述のように、ビルダーは新しい機能を提供しないため、テストする必要はありませんが、DIPはそれをAPIの一部にしているため、テストする必要があります。

これはあなたがあなたのクライアントに提示するAPIを広げるとは思いません。クライアントがインターフェースを介して使用するAPIを制限することができます(ご希望の場合)。私はまた、あなたのテストがあなたの公開されているコンポーネントを単に囲むべきではないことを提案します。それらは、そのAPIをサポートするすべてのクラス(実装)を受け入れる必要があります。例えばREST私が現在取り組んでいるシステムのAPIです:

(疑似コード)

void doSomeCalculation(json : CalculationRequestObject);

私は1つのAPIメソッドと1,000のテストを持っています。それらの大部分は、これをサポートするオブジェクトに関連しています。

0
Brian Agnew

この場合、テスト目的でないのに、なぜDIに従う必要があるのですか?あなたの公開APIだけでなくtestsIFooBuilderパラメーターなしで本当にクリーンな場合(書いたとおり)、そのようなクラスを導入しても意味がありません。 DIはそれ自体が目的ではなく、コードをよりテストしやすくするのに役立ちます。特定の抽象化レベルの自動テストを記述したくない場合は、そのレベルでDIを適用しないでください。

ただし、ビルドプロセスなしでFooDaoコンストラクターの単体テストを個別に作成できるようにDIを適用したいものの、まだFooDao(IFooBuilder fooBuilder)コンストラクターを必要としないとします。パブリックAPI、コンストラクタFooDao(IFooConnection fooConnection, IBarConnection barConnection)のみ。次に、前者を「内部」として、後者を「パブリック」として、両方を提供します。

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    // only for testing purposes!    
    internal FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    // the public API
    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }    

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

これで、FooBuilderIFooBuilderを内部に保持し、InternalsVisibleToを適用して、単体テストでアクセスできるようにすることができます。

0
Doc Brown