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


8

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

反復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の一部ではありません。

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

反復3

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

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

編集:私は私の問題であることを、それを明確にしたいではない「?どのようにテストの内部に」ではなく、それはのようなもので、「私は内部のそれを維持し、まだDIPに準拠することができ、およびIばよいですか?」。


必要に応じて、質問を編集してテストを追加できます。
Chris Wohlert、2016年

1
「FooBuilderとIFooBuilderは公開する必要があります」。確かIFooBuilderに公開する必要があるだけですか?FooBuilderを返すためIFooBuilder、内部にとどまることができる「デフォルトの実装を取得する」メソッドを介してDIシステムに公開する必要があるだけです。
David Arno

それが解決策になると思います。
Chris Wohlert、2016年

2
これは、ここではプログラマーに少なくとも12回、「publicライブラリAPIとpublicテストを区別したい」という異なる用語を使用して尋ねられています。または、「APIに表示されないようにメソッドをプライベートにしたいのですが、これらのプライベートメソッドをテストするにはどうすればよいですか?」その答えは、常に「より広範なパブリックAPIに対応する」、「パブリックAPIを介してテストに対応する」、または「メソッドinternalを作成してテストコードに表示する」です。
Doc Brown

私はおそらくその代わりとなるのではなく、新しいものを紹介してそれらをつなぎ合わせます。
RubberDuck 2016年

回答:


3

私は一緒に行きます:

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

ただし、オブジェクト指向とアジャイルデザインのSOLID原則(a 1:08:55)では、ボブおじさんは、依存関係注入に関する彼のルールはすべてを注入するのではなく、戦略的な場所にのみ注入することだと述べています。(彼はまた、依存関係の逆転と依存関係の注入のトピックは同じであると述べています)。

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


2
ボブからのこのコメントは完璧です。これを共有していただきありがとうございます。
Chris Wohlert、2016年

5

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

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

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

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

DIは間違いなく、追加のコードでオーバーヘッドが発生します。

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

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

要約すれば:

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

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

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


4
実際にそうなっているのは、一部の開発者が内部メソッドをパブリックとして公開するのではなく、内部メソッドのInternalsVisibleTo属性を選択する理由です。
ロビーディー2016年

1
「しかし、最近では、テストを簡単にするために内部に公開できるメソッドが見つかることも珍しくありません(これは通常、別のアセンブリで発生するためです)。
Brian Agnew

3
@BrianAgnewああああ。ゲッターとセッターを含む「100%の単体テストカバレッジが必要」とそのアンチパターン:-(なぜ今日の
プログラマーはそれほど考えがたいの

2
@ChrisWohlertシンプルなソリューション。俳優を交換しないでください。新しいものを追加します。それらを一緒にチェーンします。既存のテストはそのままにしておきます。それは暗黙的にあなたのビルダーをテストし、あなたのコードはまだカバーされています。価値がある場合にのみテストを記述してください。あなたの投資の収益が減少するポイントがあります。
RubberDuck

1
@ChrisWohlert 便利なコンストラクタを提供することには何の問題もありません。あなたはまだクラスの真の依存関係を注入しています。これらの "空想的な" IoCコンテナーフレームワークがすべて登場する前に、DIはどのように行われたと思いますか?
RubberDuck

0

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

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

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

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

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

再 あなたのコメント:

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

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

(疑似コード)

void doSomeCalculation(json : CalculationRequestObject);

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


適切なテストを実行するためにプライベートメソッドを(なんらかの方法で)公開する必要があるというこの概念を特に購入しません -誰もがこれを主張しているとは思いません...
ロビーディー

しかし、あなたは上記で言った-「アクセス修飾子は時々テストを簡略化するために緩められる」?
Brian Agnew 2016年

内部はテストアセンブリに公開でき、暗黙のインターフェイス実装によりパブリックメソッドが作成されます。私はプライベートメソッドを公開することを決して主張していませんでした...
ロビーディー

私のPoVから、「内部はテストアセンブリにさらされる可能性がある」と同じことになります。構造化されていないコードをリファクタリングする際の回帰テストでない限り、内部をテストすることは(プライベートとしてマークされているかどうかにかかわらず)価値のある練習であるとは確信していません
Brian Agnew

こんにちはブライアン、私はあなたと同じように、FooDaoに解析されたときにIFooBuilderを偽造しますが、これはIFooBuilderの実装のために別のテストが必要になるという意味ではありませんか?私たちはついに本当の問題に取り組み始めました。内部を公開する必要があるかどうかは問題ではありません。
Chris Wohlert、2016年

0

この場合、テスト目的でないのに、なぜDIに従う必要があるのですか?公開APIだけでなく、テストIFooBuilderパラメーター なしで本当にクリーンな場合(書いたとおり)、そのようなクラスを導入しても意味がありません。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ユニットテストでアクセスできるようにを適用できます。


あなたの質問の最初の部分はまさに私が探しているものです。質問への回答:「ここでDIPを使用すべきか」ただし、その2番目のコンストラクタは必要ありません。DIPに準拠していない場合、IFooBuilderを公開する必要はありません。偽装やテストを停止できます。要するに、あなたは、反復2に固執する私に言っている
クリスWohlert

正直なところ、「しかしながら」からすべてを削除していただければ、お答えいたします。最初の部分では、いつ、なぜDIを使用すべき/使用すべきでないのかが非常によく説明されていると思います。
Chris Wohlert、2016年

私はちょうどDIがないという警告を追加したいだけのテストについては-それは単に別で1個の結石を交換簡素化します。これが必要かどうかは、ここでしか答えられません。26の17は素晴らしく、それを置く- YAGNI&PYIAC間のどこか。
ロビーディー

@ChrisWohlert:状況に適用したくないという理由だけで、2番目の部分を削除するつもりはありません。そのレベルのテストは必要ありません-いいです、パート1を適用してください。ここではDIを使用しないでください。あなたは、テストしたい回避が公開APIに含まれる特定の部分を持っている-罰金を、あなたはとてもEveroneがどのスーツを彼に最適なソリューションを選ぶことができます部2を適用し、異なるアクセス修飾子を使用してコード内の2つのエントリポイントが必要です。
Doc Brown

確かに誰もが望むソリューションを選ぶことができますが、ビルダーをテストしたいと思った場合、あなたは質問を誤解しました。それは私が取り除くことを試みたものです。
Chris Wohlert、2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.