次のような関数を考えてみましょう。
function savePeople(dataStore, people) {
people.forEach(person => dataStore.savePerson(person));
}
次のように使用できます。
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
独自の単体テストがあるか、ベンダーが提供していると仮定しましょうStore
。いずれにせよ、私たちは信頼していStore
ます。さらに、エラー処理(たとえば、データベースの切断エラーなど)はの責任ではないと仮定しましょうsavePeople
。実際、ストア自体が魔法のようなデータベースであり、何らかのエラーが発生する可能性はないと仮定しましょう。 これらの仮定を考えると、問題は次のとおりです。
savePeople()
単体テストする必要がありますか、それとも組み込みのforEach
言語構成のテストに相当しますか?
もちろん、モックdataStore
を渡し、dataStore.savePerson()
各人に1回呼び出されることをアサートすることもできます。このようなテストは実装の変更に対するセキュリティを提供するという議論を確かに行うことができます。たとえば、forEach
従来のfor
ループや他の反復方法に置き換えることを決めた場合です。そのため、テストは完全に簡単ではありません。そしてそれでもひどく近いようです...
より実りの多い別の例を次に示します。他のオブジェクトや機能を調整する以外の何もしない機能を考えてください。例えば:
function bakeCookies(dough, pan, oven) {
panWithRawCookies = pan.add(dough);
oven.addPan(panWithRawCookies);
oven.bakeCookies();
oven.removePan();
}
このような機能は、どのようにユニットテストされるべきでしょうか?私は単純にモックていないユニットテストのいずれかの種類を想像するのは難しいdough
、pan
とoven
して、メソッドがそれらに呼ばれていると主張します。しかし、そのようなテストは、関数の正確な実装を複製する以上のことはしていません。
意味のあるブラックボックスの方法で関数をテストできないことは、関数自体の設計上の欠陥を示していますか?もしそうなら、どのように改善できますか?
bakeCookies
例の動機となる質問をさらに明確にするために、より現実的なシナリオを追加します。これは、テストを追加してレガシーコードをリファクタリングしようとしたときに遭遇したシナリオです。
ユーザーが新しいアカウントを作成するとき、いくつかのことを行う必要があります:1)新しいユーザーレコードをデータベースに作成する必要がある2)ウェルカムメールを送信する必要がある3)ユーザーのIPアドレスを詐欺のために記録する必要がある目的。
そこで、すべての「新規ユーザー」ステップを結び付けるメソッドを作成します。
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
これらのメソッドのいずれかがエラーをスローした場合、エラーを適切な方法で処理できるように、呼び出し元のコードにエラーをバブルアップする必要があることに注意してください。APIコードによって呼び出されている場合、エラーを適切なhttp応答コードに変換する場合があります。Webインターフェースによって呼び出されている場合、エラーを適切なメッセージに変換してユーザーに表示するなどのことができます。ポイントは、この関数はスローされる可能性のあるエラーを処理する方法を知らないということです。
私の混乱の本質は、そのような関数を単体テストするには、テスト自体で正確な実装を繰り返す必要があるように見えることです(メソッドが特定の順序でモックで呼び出されるように指定することによって)、それは間違っているようです。