回答:
単体テストでは、単一の方法で単一のコードパスをテストする必要があります。メソッドの実行がそのメソッドの外部で別のオブジェクトに渡され、再び戻る場合、依存関係があります。
実際の依存関係でそのコードパスをテストする場合、ユニットテストではありません。統合テストです。これは適切で必要なことですが、単体テストではありません。
依存関係にバグがある場合、誤検知を返すような方法でテストが影響を受ける可能性があります。たとえば、依存関係に予期しないnullを渡しても、依存関係がnullをスローしない場合があります。テストでは、本来あるべきようにnull引数の例外が発生せず、テストに合格します。
また、依存オブジェクトがテスト中に希望どおりの結果を確実に返すようにするのは、不可能ではないにしても難しい場合があります。これには、テスト内で予期される例外をスローすることも含まれます。
モックはその依存関係を置き換えます。依存オブジェクトの呼び出しに期待値を設定し、必要なテストを実行するために必要な正確な戻り値を設定し、例外処理コードをテストできるように、どの例外をスローするかを設定します。このようにして、問題のユニットを簡単にテストできます。
TL; DR:単体テストが関係するすべての依存関係を模擬します。
モックオブジェクトは、テスト中のクラスと特定のインターフェイス間の相互作用をテストする場合に役立ちます。
たとえば、メソッドのsendInvitations(MailServer mailServer)
呼び出しをMailServer.createMessage()
1回だけテストし、またMailServer.sendMessage(m)
1回だけ呼び出し、MailServer
インターフェイスで他のメソッドが呼び出されないことをテストします。これは、モックオブジェクトを使用できるときです。
モックオブジェクトを使用すると、実際MailServerImpl
のテストやテストを渡す代わりTestMailServer
に、MailServer
インターフェースのモック実装を渡すことができます。モックを渡す前に、モック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()
、この時点で偶然に呼び出されるすべてのメソッドに対して、適切な缶詰の戻り値を返します。どのようにして、このトレーニング方法のリストを正確に思いついたのですか?テストを実行し、テストがパスするまでメソッドを追加し続けました。なぜそれを呼び出す必要があるのか手掛かりなしにメソッドに応答するようにスタブを訓練したことに注意してください、テストが不満を述べたすべてのものを単に追加しました。テストに合格しました。
しかし、もっと派手なpdfを作成するためにsendInvitations()
、またはをsendInvitations()
使用する他のクラスを変更すると、後でどうなりますか?より多くのメソッドPdfFormatter
が呼び出され、それらを期待するようにスタブをトレーニングしなかったため、テストは突然失敗しました。そして通常、このような状況で失敗するのは1つのテストだけではなく、sendInvitations()
メソッドを直接または間接的に使用するあらゆるテストです。トレーニングを追加して、これらすべてのテストを修正する必要があります。また、不要になったメソッドがどれであるかわからないため、不要になったメソッドを削除できないことに注意してください。繰り返しますが、それはリファクタリングを妨げます。
また、テストの可読性はひどく低下しました。私たちが望んでいたために、しかし私たちがしなければならなかったために、私たちが記述しなかったコードがたくさんあります。そこにそのコードが欲しいのは私たちではありません。モックオブジェクトを使用するテストは非常に複雑に見え、多くの場合読みにくいです。テストは、読者がテスト中のクラスをどのように使用する必要があるかを理解するのに役立つはずです。したがって、それらは単純でわかりやすいものでなければなりません。それらが読めない場合、誰もそれらを維持するつもりはありません。実際、それらを維持するよりも削除する方が簡単です。
それを修正するには?簡単に:
PdfFormatterImpl
。それが不可能な場合は、実際のクラスを変更して可能にします。テストでクラスを使用できないことは、通常、クラスに問題があることを示しています。問題を修正することは双方にとってメリットのある状況です。クラスを修正し、テストを簡単にします。一方、修正せずにモックを使用することは、勝ち目のない状況です。実際のクラスを修正せず、さらに複雑で読みにくいテストがあり、さらなるリファクタリングが妨げられています。TestPdfFormatter
何もしないことを作成します。そうすれば、すべてのテストで一度変更することができ、スタブをトレーニングする長いセットアップでテストが乱雑になることはありません。全体として、モックオブジェクトには用途がありますが、慎重に使用しないと、悪意のある行為、実装の詳細のテストを促し、リファクタリングを妨げ、テストの読み取りや保守が困難になります。
モックの欠点の詳細については、「モックオブジェクト:欠点と使用例」も参照してください。
経験則:
テストする関数がパラメーターとして複雑なオブジェクトを必要とし、このオブジェクトを単にインスタンス化するのが面倒な場合(たとえば、TCP接続を確立しようとする場合)は、モックを使用します。
テストしようとしているコード単位に依存関係がある場合は、オブジェクトをモックする必要があります。
たとえば、コードの一部のロジックをテストしようとしているが、別のオブジェクトから何かを取得する必要があり、この依存関係から返されるものがテスト対象に影響を与える可能性がある場合-そのオブジェクトをモックします。
トピックに関する素晴らしいポッドキャストはここにあります