次のクラスがあるとします。
_public class A {
public void execute() {
if (something)
ThirdPartyApi.method();
}
}
_
ここで、特にexecute()
メソッドをテストします。すべてのパスがカバーされ、期待どおりに機能することを確認したいと思います。ただし、ここで発生する問題は、ThirdPartyApi
が依存関係として提供されていないため、モックできないことです。これに対する簡単な修正は、ThirdPartyApi
をこのような依存関係として提供することです。
_public class A {
private ThirdPartyApi api;
public A(ThirdPartyApi api) {
this.api = api;
}
public void execute() {
if (something)
api.method();
}
}
_
今私は私が望む機能を手に入れ、誰もが幸せになるでしょう。ただし、ここで私が気に入らないのは、クラスを完全にテスト可能にし、外部依存関係(Android API、ライブラリ、ユーティリティなど)から独立させるために、コンストラクター/セットメソッドを通じてすべての内部依存関係を提供する必要があるという事実です。 。
これは、クラスのユースケースを実現するために直接必要とされない依存関係の問題になります。これらは、単位変換ツールなどの単なるツールです。このようにして、このクラスのユーザーに、内部的に自分で作成できる依存関係を提供するように要求します。ユーザーは必要以上にクラスについて知っている必要があるため、APIの使用が難しくなります。
例をより具体的にするために、ThidPartyApi
は巨大な外部単位変換ライブラリであるとしましょう。なぜユーザーはこのライブラリからコンストラクタを通じてクラスを提供する必要があるのですか?これは、ユースケースを達成するために使用される単なる副次的なツールです。ユースケース自体とは直接関係ありません。コンストラクターでこの依存関係を要求すると、APIがさらに混乱します。次のクラスを参照してください。
_public class Storage {
private ConversionTool api;
public Storage (ConversionTool api) {
this.api = api;
}
public void storeInteger(int a) {
storeString(api.convertToString(a));
}
}
_
対
_public class Storage {
private ConversionTool api;
public Storage () {
this.api = new ConversionTool();
}
public void storeInteger(int a) {
storeString(api.convertToString(a));
}
}
_
推奨されるアプローチは何ですか?コンストラクターを介してサイド依存関係を提供すると、テストとモック化が容易になりますが、APIはその意味を失います。コンストラクターを介して副次的な依存関係を提供しないと、クラスの意味は保持されますが、クラス内のすべてをモックしてクラスを完全にテストする機能が失われます。
なぜユーザーはこのライブラリからコンストラクタを介してクラスを提供する必要があるのでしょうか?これは、ユースケースを達成するために使用される単なる副次的なツールです。
私見これは本当に重要な問題ではありません。 ThirdPartyApi
が「サイドツール」である場合は問題ではありません。代わりに、私は尋ねることをお勧めします
ThirdPartyApi
シンプルで安定した高速なテストを記述できますか?答えが「はい」の場合、クラスにThirdPartyApi
を注入する必要はありません(少なくともテスト用ではありません)。答えが「いいえ」の場合、DIがより良いアプローチになります。
「変換ライブラリ」の例は、答えが「はい」の場合のように見えます。通常、プログラミング言語または標準ライブラリの組み込み変換関数のモックを開始しないのと同じです。ただし、ThirdPartyApi
オブジェクトの構築にネットワークを介したデータベース接続のようなものが必要な場合は、いくつかの変換関数がデータベースからのデータを必要とするため、それを注入する方がより良いアプローチである可能性があります。
ThirdPartyApi
を注入可能にする理由は、単なるテスト以外にもあることに注意してください。たとえば、ベンダー固有のカップリングが多すぎないようにするため、またはさまざまな「変換戦略」を用意する必要があるためです。
DIはそれ自体が目的ではなく、目的と手段です。 「念のため」ではなく、必要なときに使用してください。
依存関係の注入とモックはどこまで行けばよいですか?
意図的にあいまいな答えは、「優れたデザインを作成するために必要な限り」です。
1972年、Parnas はモジュール設計の興味深い原理を説明しました
まず、難しい設計決定または変更される可能性のある設計決定のリストから始めます。各モジュールは、そのような決定を他のモジュールから隠すように設計されています。
テスト中に除外したいサードパーティの実装は、「変更される可能性が高い」決定のかなり良い例です。
特定の実装を提供する機能を導入しても、必ずしも必要ではありませんは、 composition root のAPIアフォーダンスがさらに複雑になることを意味します
public Storage () {
this(new ConversionTool());
}
Storage (ConversionTool api) {
this.api = api;
}
この種のアプローチはテストされていないコードパスを導入しますが、テストされていないパスはこの重要な制約を満たします。それらは「非常に単純で、明らかに欠陥がない」ということです。
関連する質問は、「テスト中にこのサードパーティのライブラリを除外しますか?」です。テストに必要なプロパティがあります-それらを頻繁に、したがって迅速に、したがって並行して、低速の副作用なしに実行できるようにしたいです。サードパーティのライブラリがこれらのプロパティを高価または不可能にする場合は、もちろん、ライブラリをテストダブルに置き換えることができるようにしたいと考えています。
サードパーティのライブラリがテスト可能である場合...とにかくそれを置き換えることができます。 Rainsbergerは彼の講演でこれを主張しています 統合テストは詐欺です 。 TL; DR-サードパーティコードのコードパスのバリエーションを考慮する必要がない場合は、設計をより効率的にテストできます。
私はこのパーティーに遅れるのを知っていますが、何か言いたいことがあります。
設計における唯一の考慮事項としてTDDの使用を中止してください!
これは決して意味がありませんでした。コードをテスト可能にするための再設計により設計が改善されるとTDD支持者が言っていたのは、ひどい誤解です。します。あなたがそれを台無しにしない場合。
はい、そうでなければ簡単にオブジェクトグラフを構築することは、構築するための悪夢に変えることが可能です。しかし、それはTDDのせいではありません。この複雑さを軽減する簡単な原則がいくつかあります。
Convention over configuration は、この問題の非常に単純な解決策です。基本的に、デフォルト値がオーバーライド可能である限り、デフォルト値をハードコードしても問題ないと述べています。このアドバイスは、名前付きパラメーター、つまりデフォルトの引数を持つ言語で実行するのは見事に簡単です。 Javaのようにそれらなしでスタックしている場合、不変であることが重要である場合、最終的にJoshua Blochsビルダーパターンでそれらをシミュレートする必要があります。ミュータブルが問題なければ、セッターはこれを簡単に解決します。 C#またはPythonなどにいる場合は、すでに名前付きパラメーターがあるため、言い訳をやめます。
別の解決策は、工場で建設を抽象化することです。 mainで直接すべてを行うと、すぐに扱いにくくなる可能性があります。良い名前を考えることができる限り、構築を単純化するハードコードされた値でファクトリをパックできますが、これが構築が行われる唯一の方法であるとは規定していません。長いコンストラクターに手を加える必要がない場合でも、長いコンストラクターの方がずっと楽なので、この方法が好きです。
これがテストのためだけの多くの作業であると考えているなら、あなたは絶対に正しいです。私はテストのためにこれをしません。私がこれを行うのは、気の利いたときにいつでもサードパーティのAPIを変更する権利を保持したくないからです。
そのためには、テストでライブラリをモックするだけではないことを確認する必要があります。ドメインオブジェクトに本当に渡す必要があるものだけを注意深く表現する必要があります。これ以上何もない。 ThirdPartyLibrary
には35の便利なメソッドがあるかもしれませんが、2つだけ必要な場合は、これら2つだけが必要であることを明確にします。
どうして?後でThirdPartyLibrary
が突然変更したい場合、私に対するライセンス契約があるので、人質にされることはありません。私は2つの愚かなメソッドを書き、私の人生を続けます。
それらを許可すると、ライブラリが言語空間に浸透しすぎて、ライブラリに言及せずにコードベースで作業する仕事を宣伝することができなくなります。それを使用する方法を決定するときそれを覚えておいてください。
大体の目安として、純粋な関数のモックを避ける必要があります。出力が入力のみに依存するもの。標準の数学ライブラリや文字列処理ルーチンをモックアウトしないのと同じ理由で、単位変換ライブラリをモックする必要はありません。
対照的に、ディスクIO、ネットワークトラフィック、データベースクエリなどをモックアウトするのには非常に十分な理由があります。これらは外部の世界に依存しており、変更可能な状態がたくさんあります。同じ呼び出し順序で同じ結果が得られるとは限りません(別のプログラムがファイルを上書きしたり、ユーザーが機内モードを有効にしたりする可能性があります)。また、断続的な障害などのまれな状況で堅牢性をテストすることもできます。
ただし、純粋な関数をモックする1つの理由はパフォーマンスです。たとえば、ゲームをテストしている場合、パスファインディングを模擬することは、直接テストされていないときにA *アルゴリズムの実行に費やす時間が短縮されるため、価値があるかもしれません。
一般的に推奨されるアプローチは、「何かを変換するもの」のインターフェースを作成し、それを依存関係として追加し、サードパーティのライブラリを使用する実装を提供することです。これにより、それらが存在しなくなった場合、高額に請求した場合、またはその他の理由で機能しなくなった場合に柔軟に対応できます。
または、それが使用されている場所に応じて(ここではStorage
)、そのクラスをサードパーティライブラリ固有の実装と見なすことができます。ただし、それを単体テストすることはできません。クラスがあまりやっていないので、それでいいこともあります。
ただし、一般的には、サードパーティのlibが1つのクラスにしか触れず、その後統合テストに必要なものだけを切り離す必要があります。