モック オブジェクトとフェイク オブジェクトの基本的な理解はありますが、モックをいつどこで使用するかについてはよくわかりません。特に、ここのシナリオに適用されるためです。
4 に答える
モック オブジェクトは、テスト対象のクラスと特定のインターフェイスの間の相互作用をテストする場合に役立ちます。
たとえば、メソッドが 1 回だけ呼び出され、さらに 1 回だけ呼び出され、インターフェイスで他のメソッドが呼び出されていないことをテストしたいsendInvitations(MailServer mailServer)
とMailServer.createMessage()
しMailServer.sendMessage(m)
ますMailServer
。これは、モック オブジェクトを使用できる場合です。
モック オブジェクトを使用すると、 realMailServerImpl
や testを渡す代わりに、インターフェースTestMailServer
のモック実装を渡すことができます。MailServer
mock を渡す前にMailServer
、それを「トレーニング」して、期待されるメソッド呼び出しと返される戻り値を認識できるようにします。最後に、モック オブジェクトは、期待されるすべてのメソッドが期待どおりに呼び出されたことをアサートします。
これは理論的には良いように思えますが、いくつかの欠点もあります。
モックの欠点
モック フレームワークを使用している場合、テスト対象のクラスにインターフェイスを渡す必要があるたびに、モック オブジェクトを使用したくなるでしょう。このようにして、必要でない場合でも相互作用をテストすることになります。残念ながら、インタラクションの不要な (偶発的な) テストは良くありません。なぜなら、特定の要件が特定の方法で実装されていることをテストすることになり、実装によって必要な結果が得られたことをテストすることになるからです。
擬似コードの例を次に示します。MySorter
クラスを作成し、それをテストしたいとしましょう。
// the correct way of testing
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert testList equals [1, 2, 3, 7, 8]
}
// incorrect, testing implementation
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert that compare(1, 2) was called once
assert that compare(1, 3) was not called
assert that compare(2, 3) was called once
....
}
(この例では、テストしたいのはクイック ソートなどの特定のソート アルゴリズムではないと想定しています。その場合、後者のテストが実際には有効です。)
このような極端な例では、後者の例が間違っている理由は明らかです。の実装を変更するときMySorter
、最初のテストでは、ソートが正しいことを確認するという素晴らしい仕事をします。これがテストの要点です。コードを安全に変更することができます。一方、後者のテストは常に失敗し、積極的に有害です。それはリファクタリングを妨げます。
スタブとしてのモック
モック フレームワークでは、多くの場合、メソッドを呼び出す回数や必要なパラメーターを正確に指定する必要がない、あまり厳密ではない使用法も許可されます。スタブとして使用されるモック オブジェクトを作成できます。
sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)
テストしたいメソッドがあるとしましょう。PdfFormatter
オブジェクトを使用して招待状を作成できます。テストは次のとおりです。
testInvitations() {
// train as stub
pdfFormatter = create mock of PdfFormatter
let pdfFormatter.getCanvasWidth() returns 100
let pdfFormatter.getCanvasHeight() returns 300
let pdfFormatter.addText(x, y, text) returns true
let pdfFormatter.drawLine(line) does nothing
// train as mock
mailServer = create mock of MailServer
expect mailServer.sendMail() called exactly once
// do the test
sendInvitations(pdfFormatter, mailServer)
assert that all pdfFormatter expectations are met
assert that all mailServer expectations are met
}
この例では、オブジェクトをあまり気にしないので、呼び出しを静かに受け入れ、この時点でたまたま呼び出したPdfFormatter
すべてのメソッドに対して適切な既定の戻り値を返すようにオブジェクトをトレーニングするだけです。sendInvitation()
どのようにしてこのトレーニング方法のリストを思いついたのでしょうか? テストを実行し、テストに合格するまでメソッドを追加し続けました。メソッドを呼び出す必要がある理由を知らずにメソッドに応答するようにスタブをトレーニングしたことに注意してください。テストで不平を言うものすべてを追加しただけです。テストに合格しました。
しかし、後で、sendInvitations()
またはを使用する他のクラスを変更sendInvitations()
して、より凝った pdf を作成するとどうなるでしょうか。PdfFormatter
より多くのメソッドが呼び出され、それらを期待するようにスタブをトレーニングしなかったため、テストは突然失敗します。通常、このような状況で失敗するのは 1 つのテストだけではなく、たまたまそのsendInvitations()
メソッドを直接的または間接的に使用するすべてのテストです。トレーニングを追加して、これらすべてのテストを修正する必要があります。また、不要になったメソッドを削除することはできないことに注意してください。どのメソッドが不要なのかわからないためです。繰り返しますが、それはリファクタリングを妨げます。
また、test の可読性はひどく損なわれました。書きたかったからではなく、書かなければならなかったために書いたコードがたくさんあります。そこにそのコードが欲しいのは私たちではありません。モック オブジェクトを使用するテストは非常に複雑に見え、読みにくいことがよくあります。テストは、読者がテスト対象のクラスをどのように使用すべきかを理解するのに役立つ必要があるため、単純でわかりやすいものにする必要があります。それらが読めない場合、誰もそれらを維持するつもりはありません。実際、それらを維持するよりも削除する方が簡単です。
それを修正する方法は?簡単に:
- 可能な限り、モックの代わりに実際のクラスを使用してみてください。実物を使用
PdfFormatterImpl
。それが不可能な場合は、実際のクラスを変更して可能にします。テストでクラスを使用できないということは、通常、そのクラスに何らかの問題があることを示しています。問題を修正することは双方に有利な状況です。クラスを修正すると、テストがより簡単になります。一方、それを修正せずにモックを使用することは、勝ち目のない状況です。実際のクラスを修正せず、より複雑で読みにくいテストがあり、それ以上のリファクタリングを妨げます。 - 各テストでインターフェイスをモックするのではなく、インターフェイスの単純なテスト実装を作成してみて、すべてのテストでこのテスト クラスを使用してください。
TestPdfFormatter
何もしないものを作成します。そうすれば、すべてのテストに対して一度変更することができ、スタブをトレーニングする長いセットアップでテストが雑然とすることはありません。
全体として、モック オブジェクトには用途がありますが、慎重に使用しないと、しばしば悪い慣行を助長し、実装の詳細をテストし、リファクタリングを妨げ、読みにくく保守しにくいテストを生成します。
モックの欠点の詳細については、モック オブジェクト: 欠点とユース ケースも参照してください。
単体テストでは、単一のメソッドを使用して単一のコードパスをテストする必要があります。メソッドの実行がそのメソッドの外に出て、別のオブジェクトに渡され、再び戻ってくる場合、依存関係があります。
実際の依存関係でそのコード パスをテストする場合、単体テストではありません。あなたは統合テストを行っています。それは良いことであり、必要なことですが、単体テストではありません。
依存関係にバグがある場合、テストは誤検知を返すような影響を受ける可能性があります。たとえば、依存関係に予期しない null を渡す場合がありますが、文書化されているように、依存関係が null をスローしない場合があります。テストは、本来あるべき null 引数の例外に遭遇せず、テストに合格します。
また、依存オブジェクトがテスト中に必要なものを正確に返すように確実に取得することは、不可能ではないにしても難しい場合があります。これには、テスト内で予想される例外をスローすることも含まれます。
モックはその依存関係を置き換えます。依存オブジェクトへの呼び出しに対する期待値を設定し、必要なテストを実行するために必要な正確な戻り値を設定し、スローする例外を設定して、例外処理コードをテストできるようにします。このようにして、問題のユニットを簡単にテストできます。
TL;DR: 単体テストが触れるすべての依存関係をモックします。
経験則:
テストしている関数がパラメーターとして複雑なオブジェクトを必要とし、このオブジェクトを単純にインスタンス化するのが面倒な場合(たとえば、TCP接続を確立しようとする場合)、モックを使用します。
テストしようとしているコード単位に依存関係があり、それが「まさにそう」である必要がある場合は、オブジェクトをモックする必要があります。
たとえば、コード単位でいくつかのロジックをテストしようとしているが、別のオブジェクトから何かを取得する必要があり、この依存関係から返されるものがテストしようとしているものに影響を与える可能性がある場合、そのオブジェクトをモックします。
このトピックに関する素晴らしいポッドキャストはここにあります