アプリケーションコードと単体テストのインターフェイス


8

私は、いくつかの新しいモジュールを実装してユニットテストする必要があるプロジェクトに取り組んでいます。私はかなり明確なアーキテクチャを念頭に置いていたので、メインのクラスとメソッドをすぐに書き留めてから、単体テストの作成を開始しました。

テストを書いている間、次のような元のコードにかなりの修正を加える必要がありました。

  • それらをテストするためにプライベートメソッドをパブリックにする
  • プライベート変数にアクセスするためのメソッドを追加する
  • ユニットテスト内でコードを実行するときに使用するモックオブジェクトを挿入するためのメソッドを追加します。

どういうわけか私はこれらが私たちが何か間違ったことをしている症状であると感じています、例えば

  1. 最初の設計が間違っていた(一部の機能は最初から公開されているはずでした)、
  2. コードは、単体テストとのインターフェース用に適切に設計されていません(おそらく、かなりの数のクラスが既に設計されているときに単体テストの設計を開始したという事実が原因です)。
  3. ユニットテストを間違った方法で実装している(たとえば、ユニットテストはAPIのパブリックメソッドを直接テスト/アドレスするだけであり、プライベートメソッドをアドレスするべきではない)。
  4. 上記の3つのポイントの混合、およびおそらく私が考えていなかったいくつかの追加の問題。

単体テストの経験はあるものの、第一人者にはほど遠いので、これらの問題についてのあなたの考えを読んでいただければ非常に興味があります。

上記の一般的な質問に加えて、より具体的で技術的な質問があります。

質問1.クラスAのプライベートメソッドmを直接テストし、それをテストするためにパブリックにすることは理にかなっていますか?または、mを呼び出す他のパブリックメソッドを対象とする単体テストによってmが間接的にテストされると想定する必要がありますか?

質問2.クラスAのインスタンスにクラスB(複合集約)のインスタンスが含まれている場合、AをテストするためにBを模擬することは理にかなっていますか?私の最初のアイデアは、BインスタンスはAインスタンスの一部であるため、Bをモック化しないことですが、それからこれについて疑い始めました。Bをモックすることに対する私の議論は、1と同じです:Bはプライベートwrt Aであり、その実装にのみ使用されます。しかし、これらの問題は設計上の欠陥を示している可能性があります。おそらく、複合集約ではなく、AからBへの単純な関連付けを使用する必要があります。

質問3.上記の例で、Bをモックすることにした場合、どのようにBインスタンスをAに注入しますか?ここに私たちが持っていたいくつかのアイデアがあります:

  • AコンストラクターでBインスタンスを作成する代わりに、Aインスタンスの引数としてBインスタンスを注入します。
  • BFactoryインターフェースを引数としてAコンストラクターに渡し、Aがファクトリーを使用してプライベートBインスタンスを作成できるようにします。
  • AにプライベートなBFactoryシングルトンを使用します。シングルトンを設定するには、静的メソッドA :: setBFactory()を使用します。Aが設定されている場合(テストシナリオ)、ファクトリシングルトンを使用するBインスタンスを作成する場合、シングルトンが設定されていない場合(実稼働コードシナリオ)、Bを直接作成します。

最初の2つの代替案は私にはきれいに見えますが、Aコンストラクターのシグニチャーを変更する必要があります。APIを変更してテストしやすくすることは、私には不便に思えますが、これは一般的な方法ですか?

3番目の方法には、コンストラクターのシグニチャーを変更する必要がない(APIへの変更の影響が少ない)利点がありますが、テストを開始する前に静的メソッドsetBFactory()を呼び出す必要があります。これはIMOエラーが発生しやすい(テストが適切に機能するためのメソッド呼び出しへの暗黙の依存関係)。どちらを選べばいいのかわかりません。


C ++のフレンドクラス/関数機能が役立つと思います。試しましたか?
Mert Akcakaya、2012年

@Mert:まだ試していません。質問:フレンドを使用する場合、テストコードクラスをメインコードクラスのフレンドとして宣言する必要があります。これでいい?テストコードに応じて製品コードを用意します。これは良い考えですか?それとも、あなたが考えていた別の解決策でしたか?
ジョルジオ

私はC ++の専門家ではありません。単純な解決策として頭に浮かんだだけです。
Mert Akcakaya、2012年

回答:


8

ほとんどの場合、パブリックメソッドのテストで十分だと思います。

プライベートメソッドが非常に複雑な場合は、それらをパブリックメソッドとして別のクラスに配置し、元のクラスのそれらのメソッドへのプライベートコールとして使用することを検討してください。これにより、元のクラスとユーティリティクラスの両方のメソッドが正しく動作することを確認できます。

プライベートメソッドに大きく依存することは、設計の設計について考慮する必要があります。


私はあなたに同意します:プライベートメソッドをテストする必要があると感じた場合、そもそもそれはプライベートではなく、個別にテストする必要がある別のユーティリティクラスに置く必要があります。
ジョルジオ

すべてのパブリックメソッドをテストすると、すべてのプライベートメソッドが既にテスト(読み取り:カバー)されているはずです。そうでなければ、彼らはそこで何をしていますか?:)
アマデウスハイン

1
@Amadeus Heing、パブリックメソッドのテストでは、プライベートメソッドを呼び出すだけでテストはできません。
Mert Akcakaya 2012年

2
@Mertはい、ただし、一般に、プライベートメソッドをテストすることは、コード内で何か他の問題があることを示すシグナルです。詳細:リンク
アマデウスハイン

1
「プライベートメソッドに大きく依存することは、設計の設計について考慮する必要があります。」:私たちの場合、プライベートメソッドは、パブリックメソッドによって一度呼び出されるユーティリティメソッドでした。しかし、それらをテストする方がより堅牢であるかどうか疑問に思いました。しかし、おそらく多くの人が指摘したように、これらのメソッドをユーティリティクラスに移動し、それらをテストしたいほど重要な場合は、それらをパブリックにすることは理にかなっています。
ジョルジオ

5

質問1に応じて異なります。通常、パブリックメソッドの単体テストから始めます。Aに対してプライベートにしたいメソッドmに遭遇することがありますが、mを単独でテストすることも理にかなっていると思います。その場合は、mを公開するか、テストTestAクラスをAます。ただし、mをテストするための単体テストを追加すると、後で署名やmの動作を変更することが少し難しくなります。Aの「実装の詳細」を維持したい場合は、直接単体テストを追加しない方がよいでしょう。

質問2:(C ++ビルトイン)インスタンスをモックアウトする場合、複合集計はうまく機能しません。実際、Bの構築はAのコンストラクターで暗黙的に行われるため、外部から依存関係を注入することはできません。それが問題である場合は、Aをテストする方法に依存します。BではなくBのモックを使用して、Aを単独でテストする方が理にかなっていると思われる場合は、プレーンな関連付けを使用することをお勧めします。Bをモックアウトすることなく、Aに必要なすべてのユニットテストを記述できると思う場合、コンポジットはおそらく問題ありません。

質問3:APIに依存するコードがあまりない限り、APIを変更してテストを容易にすることが一般的です。TDDを実行しているとき、後でテストしやすくするためにAPIを変更する必要はありません。最初に、テストしやすいように設計されたAPIから始めます。後でAPIを変更してテストしやすくする場合は、問題が発生する可能性がありますが、それは事実です。したがって、APIを簡単に変更できる限り、あなたが説明した1つ目または2つ目の選択肢を使い、3つ目の選択肢のようなものを使用します(注:これはシングルトンパターンなしでも機能します)。いかなる状況でもAPIを変更しないでください。

あなたが「間違っている」かもしれないというあなたの懸念について:すべての大きなエンジンまたはマシンにはメンテナンスの穴があるので、ソフトウェアにそのようなものを追加する必要があるという私見はそれほど驚くべきことではありません。


2
+1最後の段落。エレクトロニクスの世界がどのようにテストするかについての良い例の研究のために?
mattnz 2012年

+1:非常にやる気のある回答をありがとう。私の主な懸念の1つは、アプリケーションコードがテストコードではなくアプリケーションコードに機能を提供することです。テストコードは、要件を課すことなくアプリケーションコードを監視する必要があります。もちろん、コードをより見やすくするためにいくつかの要件があるかもしれませんが、これらは本当に最小限でなければなりません。複合例を参照してください。IMOは、テスト容易性ではなく、アプリケーションドメインの要件に基づいて、単純な関連に関する複合を選択する必要があります。テスト要件へのIMO曲げアプリケーション要件は、最後の手段でなければなりません。
ジョルジオ

1
@Giorgio:ここでの誤解は、アソシエーションとコンポジットの使用はドメイン要件とは何の関係もないということです。両方の種類の設計であらゆるドメイン要件を満たすことができます。ソフトウェアをよりテストしやすくすることは、最小限の変更を加えるだけで達成できると期待できることではありません。正しく行うと、設計レベルでソフトウェアに確実に影響します。
Doc Brown

@Doc Brown:ええと、A <>-Bがコンポジットの場合、Bのインスタンスは、Aのインスタンスによって管理され、その存続期間がAの存続期間によって制御される場合にのみ存在できます。これはドメイン要件になる可能性があります。一方、単純な「使用」関連付けA-> Bは、AインスタンスがBインスタンスを管理する必要があることを強制しません。おそらく私たちのケースでは、アプリケーションドメインの分析に本当に欠陥がありました(構成の代わりに関連付けを使用する必要があります)。
ジョルジオ

@ジョルジオ:AとBのインスタンスが同じ存続期間を持つ必要がある場合があります。しかし、それをどのように満たすかはあなた次第です。C++ビルトイン形式のコンポジションを使用してこれを解決することを強制するドメイン要件はありません。AとBを分離してテストできるようにするには、少なくともテストでは、BなしでAのインスタンスを作成する必要があります。逆も同様です。そのため、この場合は、コンパイル時メカニズムの代わりに、ランタイムメカニズムを使用して(たとえば、スマートポインターを使用して)これらのオブジェクトの有効期間を制御します。
Docブラウン

1

依存性注入と制御の反転を調べる必要があります。Misko Heveryは彼のブログで多くのことを説明しています。DIとIoCは、単体テストとモックが簡単なコードを作成するための設計パターンです。

質問1:いいえ、プライベートメソッドをパブリックにしてテストしないでください。メソッドが十分に複雑な場合は、そのメソッドだけを含むコラボレータークラスを作成し、それを他のクラスに注入(コンストラクターに渡す)できます。1

質問2:この場合、誰がAを作成しますか?Aを構築するファクトリー/ビルダークラスがある場合、コラボレーターBをコンストラクターに渡しても害はありません。Aがそれを使用するコードとは別のパッケージ/名前空間にある場合は、コンストラクターをパッケージプライベートにして、ファクトリー/ビルダーがそれを構築できる唯一のクラスになるようにすることもできます。

質問3:私は質問2でこれに回答しました。

  • ビルダー/ファクトリーパターンを使用すると、クラスを使用するコードを使いにくくすることを心配する必要なく、必要なだけ依存性注入を実行できます。
  • オブジェクトの構築時間オブジェクトの使用時間を分離することで、APIを使用するコードをより簡単にすることができます。

1 これはC#/ Javaの答えです-C ++にはこれを容易にする追加機能があるかもしれません

あなたのコメントへの回答として:

私が意味したことは、あなたの生産コードがから変更されることでした(私の疑似C ++を許してください):

void MyClass::MyUseOfA()
{
  A* a = new A();
  a->SomeMethod();
}

A::A()
{
  m_b = new B();
}

に:

void MyClass::MyUseOfA()
{
  A* a = AFactory.NewA();
  a->SomeMethod();
}

A* AFactory::NewA()
{
  // Construct dependencies
  B* b = new B();
  return new A(b);
}

A::A(B* b)
{
  m_b = b;
}

次に、テストは次のようになります。

void MyTest::TestA()
{
  MockB* b = new MockB();
  b->SetSomethingInteresting(somethingInteresting);

  A* a = new A(b);

  a->DoSomethingInteresting();

  b->DidSomethingInterestingHappen();
}

このように、ファクトリを渡す必要はありません。Aを呼び出すコードは、Aを構築する方法を知る必要がなく、テストは、動作をテストできるようにAをカスタム構築できます。

他のコメントで、ネストされた依存関係について尋ねました。したがって、たとえば、依存関係が次の場合は、

A-> C-> D-> B

最初に尋ねる質問は、AがCとDを使用するかどうかです。使用しない場合、なぜAに含まれているのですか?それらが使用されていると仮定すると、おそらくファクトリでCを渡し、テストでMockBを返すMockCを構築して、可能なすべての対話をテストできるようにする必要があります。

これが複雑になり始めている場合は、デザインが結合しすぎている可能性があります。結合を緩めて凝集度を高く保つことができれば、この種のDIは実装が容易になります。


回答2について:プロダクションコードとテストコードがAを構築するために2つの異なるファクトリ実装を使用することを意味しますか?ここで、(1)プロダクションコードファクトリはプロダクションBインスタンスを注入し、(2)テストコードファクトリはBモックインスタンス。実際、Bインスタンスは、コンポジションツリーで非常に深くネストされています。ファクトリーをコンポジションツリーのいくつかのレベルに渡す必要があります。Aのインスタンスは、その親オブジェクト(他のクラス)によって構築されます
ジョルジオ

質問3に関する問題の1つは、BファクトリをAに注入する方法です。Aコンストラクターのコンストラクター引数を使用して、ファクトリーへのローカル参照を設定するか、Aがファクトリーを使用する必要があるときにアクセスするシングルトンとして。
ジョルジオ

@ Giorgioあなたのコメントについて私の更新を見てください。あなたの具体的な例がわからないので、私の一般的な例は当てはまらないかもしれませんが、これは、テストの問題を単純化できるかどうかを確認するために私が取るアプローチの一種です。
Bringer128

あなたの例をたくさんありがとう(私は疑似コードは大丈夫だと思います)。2つの観察:(1)プロダクションコードでファクトリを使用し、テストコードでプレーンコンストラクタを使用する理由 (2)組成階層はC - > D - > A - > B、及びAにCから注入されなければならないMockBインスタンス提供しなければならないCのユーザ
Giorgioの

(1)ファクトリーは、Aを使用するコードからDIアスペクトを非表示にすることです。これは、DIを追加するためにコードがさらに複雑になるのを防ぐことを目的としています。より正確には、A、B、C、Dからも依存関係管理を抽象化できます。(2)提供している例は、実際には単体テストそのものではありません。Cでのみメソッドを呼び出す場合、Aで高いテストカバレッジを取得するのははるかに難しくなります。これがどれほど重要であるかはあなた次第ですが、単体テストではA、Bとの相互作用、およびその戻り値のみをテストする必要があります。
Bringer128
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.