TDD と DI:依存関係の注入が面倒になる
-
03-07-2019 - |
質問
C#、nUnit、Rhino モック (該当する場合)。
TDD に関する私の探求は、複雑な関数の周囲にテストをラップする試みとして続きます。たとえば、保存時にフォーム内の依存オブジェクトも保存する必要があるフォームをコーディングしているとします。フォームの質問への回答、添付ファイル (利用可能な場合)、および「ログ」エントリ (「フォームを更新しました」など)。 「なんとかファイルを添付しました。」)。この保存機能では、保存機能中にフォームの状態がどのように変化したかに応じて、さまざまなユーザーに電子メールが送信されます。
つまり、フォームの保存関数とそのすべての依存関係を完全にテストするには、5 つまたは 6 つのデータ プロバイダーを挿入してこの 1 つの関数をテストし、すべてが正しい方法と順序で起動されることを確認する必要があります。これは、モックされたプロバイダーを挿入するためにフォーム オブジェクトの複数のチェーンされたコンストラクターを作成するときに面倒です。リファクタリングの方法か、モックされたデータプロバイダーを設定するためのより良い方法のいずれかで、何かが欠けていると思います。
この関数をどのように簡略化できるかを確認するために、リファクタリング方法をさらに研究する必要がありますか?依存オブジェクトが親フォームが保存されたときを検出し、それ自体を処理するために、オブザーバー パターンはどのように聞こえますか?テストできるように関数を分割するべきだと人々が言うことは知っています...つまり、各依存オブジェクトの個別の保存関数をテストしますが、フォーム自体の保存関数はテストしません。フォーム自体の保存関数は、各オブジェクトがどのように自分自身を保存するかを決定します。最初の場所?
解決
AutoMockingコンテナを使用します。 RhinoMocks用に書かれたものがあります。
コンストラクタ注入を介して注入された多くの依存関係を持つクラスがあると想像してください。 AutoMockingコンテナではなくRhinoMocksで設定すると、次のようになります。
private MockRepository _mocks;
private BroadcastListViewPresenter _presenter;
private IBroadcastListView _view;
private IAddNewBroadcastEventBroker _addNewBroadcastEventBroker;
private IBroadcastService _broadcastService;
private IChannelService _channelService;
private IDeviceService _deviceService;
private IDialogFactory _dialogFactory;
private IMessageBoxService _messageBoxService;
private ITouchScreenService _touchScreenService;
private IDeviceBroadcastFactory _deviceBroadcastFactory;
private IFileBroadcastFactory _fileBroadcastFactory;
private IBroadcastServiceCallback _broadcastServiceCallback;
private IChannelServiceCallback _channelServiceCallback;
[SetUp]
public void SetUp()
{
_mocks = new MockRepository();
_view = _mocks.DynamicMock<IBroadcastListView>();
_addNewBroadcastEventBroker = _mocks.DynamicMock<IAddNewBroadcastEventBroker>();
_broadcastService = _mocks.DynamicMock<IBroadcastService>();
_channelService = _mocks.DynamicMock<IChannelService>();
_deviceService = _mocks.DynamicMock<IDeviceService>();
_dialogFactory = _mocks.DynamicMock<IDialogFactory>();
_messageBoxService = _mocks.DynamicMock<IMessageBoxService>();
_touchScreenService = _mocks.DynamicMock<ITouchScreenService>();
_deviceBroadcastFactory = _mocks.DynamicMock<IDeviceBroadcastFactory>();
_fileBroadcastFactory = _mocks.DynamicMock<IFileBroadcastFactory>();
_broadcastServiceCallback = _mocks.DynamicMock<IBroadcastServiceCallback>();
_channelServiceCallback = _mocks.DynamicMock<IChannelServiceCallback>();
_presenter = new BroadcastListViewPresenter(
_addNewBroadcastEventBroker,
_broadcastService,
_channelService,
_deviceService,
_dialogFactory,
_messageBoxService,
_touchScreenService,
_deviceBroadcastFactory,
_fileBroadcastFactory,
_broadcastServiceCallback,
_channelServiceCallback);
_presenter.View = _view;
}
今、AutoMockingコンテナの場合と同じものがあります:
private MockRepository _mocks;
private AutoMockingContainer _container;
private BroadcastListViewPresenter _presenter;
private IBroadcastListView _view;
[SetUp]
public void SetUp()
{
_mocks = new MockRepository();
_container = new AutoMockingContainer(_mocks);
_container.Initialize();
_view = _mocks.DynamicMock<IBroadcastListView>();
_presenter = _container.Create<BroadcastListViewPresenter>();
_presenter.View = _view;
}
簡単ですか?
AutoMockingコンテナーは、コンストラクター内のすべての依存関係のモックを自動的に作成します。次のようにテストのためにモックにアクセスできます。
using (_mocks.Record())
{
_container.Get<IChannelService>().Expect(cs => cs.ChannelIsBroadcasting(channel)).Return(false);
_container.Get<IBroadcastService>().Expect(bs => bs.Start(8));
}
役立つことを願っています。 AutoMockingコンテナの登場により、私のテストライフが非常に簡単になりました。
他のヒント
まず、TDDをフォローしている場合、複雑な関数をテストでラップしません。テストを関数でラップします。実際、それでも正しくありません。テストと関数を織り交ぜながら、ほぼ同時に両方を作成し、テストを関数の少し先に進めます。 TDDの3つの法則をご覧ください。
これらの3つの法則に従い、リファクタリングに熱心であれば、「複雑な機能」で終わることはありません。むしろ、多くのテスト済みのシンプルな関数を作成します。
今、あなたのポイントに。 「複雑な機能」が既にある場合テストをラップする場合は、次を実行する必要があります。
- DIではなく、モックを明示的に追加します。 (たとえば、「テスト」フラグや、実際のオブジェクトの代わりにモックを選択する「if」ステートメントのような恐ろしいもの)。
- コンポーネントの基本的な動作をカバーするために、いくつかのテストを作成します。
- 容赦なくリファクタリングし、複雑な関数を多くの小さな単純な関数に分割し、可能な限り頻繁にテストを一緒に実行します。
- 「テスト」フラグをできるだけ高く押します。リファクタリング時に、データソースを小さな単純な関数に渡します。 「テスト」フラグを最上位の機能以外に感染させないでください。
- テストを書き換えます。リファクタリングするときは、できるだけ多くのテストを書き直して、大きなトップレベル関数の代わりに単純な小さな関数を呼び出します。モックをテストから単純な関数に渡すことができます。
- 「テスト」フラグを取り除き、本当に必要なDIの量を判断します。下位レベルで記述されたテストを使用して、引数を介してモックを挿入できるため、おそらくトップレベルで多くのデータソースをモックアウトする必要はおそらくないでしょう。
これでもやはりDIが面倒な場合は、すべてのデータソースへの参照を保持する単一のオブジェクトを挿入することを検討してください。多数のものを注入するよりも、1つのものを注入する方が常に簡単です。
それは面倒なことです。
モッキング方法論の支持者は、コードが不適切に記述されていることを指摘するでしょう。つまり、このメソッド内で依存オブジェクトを構築しないでください。むしろ、注入APIには適切なオブジェクトを作成する関数が必要です。
6つの異なるオブジェクトのモックアップに関しては、それは事実です。ただし、これらのシステムの単体テストも行っていた場合、これらのオブジェクトには使用可能なモッキングインフラストラクチャが既にあるはずです。
最後に、いくつかの作業を行うモックフレームワークを使用します。
あなたのコードはありませんが、私の最初の反応は、あなたのオブジェクトがあまりにも多くの協力者を持っていることをテストがあなたに伝えようとしていることです。このような場合、私は常に、より高いレベルの構造にパッケージ化する必要のある構造が欠落していることに気付きます。オートモックコンテナを使用することは、テストから得られるフィードバックを抑えるだけです。 http://www.mockobjects.com/2007/04を参照してください。 /test-smell-bloated-constructor.html で詳細を議論してください。
この文脈では、通常、「これは、オブジェクトの依存関係が多すぎることを示しています」または「オブジェクトのコラボレーターが多すぎる」というような記述は、かなり疑わしい主張であることがわかります。もちろん、MVC コントローラーまたはフォームは、その義務を果たすためにさまざまなサービスやオブジェクトを呼び出すことになります。結局のところ、アプリケーションの最上位層に位置しています。これらの依存関係の一部をまとめてより高いレベルのオブジェクトにまとめることができます (たとえば、 ShippingMethodRepository と TransitTimeCalculator を ShippingRateFinder に結合するなど) が、これは、特にこれらのトップレベルのプレゼンテーション指向のオブジェクトの場合には限界があります。これによりモックするオブジェクトが 1 つ減りましたが、実際の依存関係を 1 つの間接層によって難読化しただけであり、実際に削除したわけではありません。
冒涜的なアドバイスの 1 つは、オブジェクトに依存関係を挿入し、変更される可能性が非常に低いインターフェイスを作成している場合 (コードを変更するときに、本当に新しい MessageBoxService を追加するつもりですか?) ということです。本当に?)、それでは気にしないでください。この依存関係はオブジェクトの予期される動作の一部であり、実際のビジネス価値が存在するのは統合テストであるため、それらを一緒にテストするだけで済みます。
もう 1 つの冒涜的なアドバイスは、MVC コントローラーや Windows フォームの単体テストには通常、ほとんど有用性が見られないということです。誰かが HttpContext を嘲笑し、Cookie が設定されているかどうかをテストしているのを見るたびに、私は叫びたくなります。AccountController が Cookie を設定したかどうかを誰が気にするでしょうか?私はしません。Cookie は、コントローラーをブラック ボックスとして扱うこととは何の関係もありません。統合テストは、その機能をテストするために必要なものです (うーん、統合テストで Login() の後に PrivilegedArea() の呼び出しが失敗しました)。こうすることで、ログイン Cookie の形式が変更された場合でも、100 万件の無駄な単体テストが無効になることを回避できます。
オブジェクト モデルの単体テストを保存し、プレゼンテーション層の統合テストを保存し、可能な場合はモック オブジェクトを避けます。特定の依存関係をモックするのが難しい場合は、現実的になりましょう。単体テストを行わずに、代わりに統合テストを作成して、時間を無駄にするのをやめてください。
簡単な答えは、テストしようとしているコードがやり過ぎです。 単一責任の原則を守ることが役立つと思います。
[保存]ボタンメソッド他のオブジェクトに物事を委任するためのトップレベルの呼び出しのみを含める必要があります。これらのオブジェクトは、インターフェースを介して抽象化できます。次に、[保存]ボタンメソッドをテストするときは、模擬オブジェクトとの相互作用のみをテストします。
次のステップは、これらの下位レベルのクラスにテストを記述することですが、これらを単独でテストするだけなので、作業は簡単になるはずです。複雑なテストセットアップコードが必要な場合、これは設計の誤り(またはテストアプローチの誤り)の良い指標です。
推奨読書:
DIを行う唯一の方法は、コンストラクターDIではありません。 C#を使用しているため、コンストラクターが重要な作業を行わない場合は、Property DIを使用できます。これにより、オブジェクトのコンストラクターの点で物事が大幅に簡素化されますが、関数が複雑になります。関数は、動作を開始する前に、依存するプロパティがnullかどうかをチェックし、nullの場合はInvalidOperationをスローする必要があります。