ユニットテストアプリケーションロジックと不信な言語構成要素の境界線はどこですか?


87

次のような関数を考えてみましょう。

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();
}

このような機能は、どのようにユニットテストされるべきでしょうか?私は単純にモックていないユニットテストのいずれかの種類を想像するのは難しいdoughpanovenして、メソッドがそれらに呼ばれていると主張します。しかし、そのようなテストは、関数の正確な実装を複製する以上のことはしていません。

意味のあるブラックボックスの方法で関数をテストできないことは、関数自体の設計上の欠陥を示していますか?もしそうなら、どのように改善できますか?


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インターフェースによって呼び出されている場合、エラーを適切なメッセージに変換してユーザーに表示するなどのことができます。ポイントは、この関数はスローされる可能性のあるエラーを処理する方法を知らないということです。

私の混乱の本質は、そのような関数を単体テストするには、テスト自体で正確な実装を繰り返す必要があるように見えることです(メソッドが特定の順序でモックで呼び出されるように指定することによって)、それは間違っているようです。


44
実行後。クッキーはありますか
ユアン

6
アップデートに関して:なぜパンをモックしたいのですか?または生地?これらは簡単に作成できるメモリ内オブジェクトのように聞こえるので、1つのユニットとしてテストしない理由はありません。「ユニットテスト」の「ユニット」は「単一のクラス」を意味しないことを忘れないでください。「何かを成し遂げるために使用されるコードの最小単位」を意味します。パンは、おそらく生地のオブジェクトのコンテナ以外の何ものでもありませんので、だけでなく、テスト駆動で外部からbakeCookies方法を単離し、それをテストするために工夫されたい。
サラ

11
結局のところ、ここで働く基本的な原則は、コードが機能すること、そして誰かが何かを変更したときに適切な「炭鉱のカナリア」であることを確認するのに十分なテストを書くことです。それでおしまい。魔法の呪文、定型的な仮定、または独断的な主張はありません
ロバートハーベイ

5
@RobertHarveyは、残念ながら定型的な決まり文句とTDDの音に噛まれますが、熱狂的な同意を得るのは確かですが、現実の問題を解決する助けにはなりません。そのためには、手を汚し、実際の質問に答えるリスクがある必要があります
ジョナ

4
サイクロマティックの複雑さの順にユニットテスト。私を信じて、この機能に到達する前に時間を使い果たします
ニールマクギガン

回答:


118

savePeople()ユニットをテストする必要がありますか?はい。dataStore.savePerson動作するか、db接続が動作するか、動作するかをテストしていませんforeach。あなたはsavePeople、その契約を通して約束を果たすテストをしています。

このシナリオを想像してください。誰かがコードベースの大きなリファクタリングを行い、誤っforEachて実装の一部を削除して、常に最初の項目のみを保存するようにします。単体テストでそれを捕まえてみませんか?


20
@RobertHarvey:多くの灰色の領域があり、区別すること、IMOは重要ではありません。ただし、「正しい関数を呼び出す」ことをテストすることは実際には重要ではなく、どのように実行するかに関係なく「正しいことを実行する」ことは重要です。重要なのは、関数への特定の入力セットが与えられると、特定の出力セットが得られることをテストすることです。しかし、最後の文がどのように紛らわしいのかがわかるので、削除しました。
ブライアンオークリー

64
「savePeopleが契約を通じて約束したことを満たしていることをテストしています。」この。そんなに。
ロヴィス

2
それをカバーする「エンドツーエンド」システムテストがない限り。
イアン

6
@Ianエンドツーエンドテストは単体テストに置き換わるものではなく、無料です。人々のリストを保存するためのエンドツーエンドのテストがあるかもしれないからといって、それをカバーするための単体テストが必要ないという意味ではありません。
ビンセントサバード

4
@VincentSavard、ただし、リスクが別の方法で制御されている場合、単体テストのコスト/利点は削減されます。
イアン

36

通常、この種の質問は、人々が「テスト後」の開発を行うときに発生します。実装の前にテストが行​​われるTDDの観点からこの問題にアプローチし、演習としてこの質問をもう一度自問してください。

少なくともTDDのアプリケーション(通常は外部からのアプリケーション)では、を実装したsavePeople後のような関数を実装しませんsavePersonsavePeopleそしてsavePerson関数として開始すると、同じユニットテストからテスト駆動されます。リファクタリングのステップで、いくつかのテストの後に2つの間の分離が発生します。また、この仕事のモードは、機能がどこにあるsavePeopleべきであるかという問題を提起するでしょう-それが無料の機能であるかそれともdataStore

最後に、テストはあなたが正しく保存することができた場合のみチェックしていないだろうPersonではStoreなく、多くの人。また、他のチェックが必要かどうか、たとえば、「savePeople関数がアトミックであることを確認する必要がありますか、すべてを保存するか、まったく保存しないか」、「どうにかしてエラーを返すことができる人にエラーを返すことができますか?」保存されますか?これらのエラーはどのように表示されますか?」など。これはすべてforEach、反復の使用や他の形式のチェックだけではありません。

ただし、一度に複数の人を救うという要件savePersonがすでに達成された後にだけ来た場合は、既存のテストを更新savePersonして新しい関数を実行し、savePeople最初に委任するだけで一人を救うことができることを確認し、次に、新しいテストを通じて複数の人の動作をテスト駆動し、動作をアトミックにするかどうかを検討します。


4
基本的に、実装ではなくインターフェイスをテストします。
スヌープ

8
公平で洞察に満ちたポイント。それでも、どういうわけか私の本当の質問は避けられているように感じます:)あなたの答えは、「現実の世界では、うまく設計されたシステムでは、あなたの問題のこの単純化されたバージョンが存在するとは思わない」と言っています。繰り返しますが、より一般的な問題の本質を強調するために、この単純化されたバージョンを具体的に作成しました。例の人為的な性質を超えられない場合、おそらく、反復と委任のみを行う同様の関数の正当な理由がある別の例を想像できます。それとも単に不可能だと思いますか?
ジョナ

@ジョナが更新されました。それがあなたの質問にもう少し良く答えることを願っています。これはすべて非常に意見に基づいており、このサイトの目標に反する可能性がありますが、確かに非常に興味深い議論になります。ちなみに、私は専門的な仕事の観点から答えようとしました。そこでは、どんなに些細な実装であっても、すべてのアプリケーションの振る舞いに対して単体テストを残すよう努力しなければなりません。私たちが去る場合、新しいメンテナーのための文書化されたシステム。個人的なプロジェクトや、たとえば重要ではない(お金も重要)プロジェクトについては、私は非常に異なる意見を持っています。
ミシェルヘンリッヒ

更新していただきありがとうございます。どのくらい正確にテストしsavePeopleますか?OPの最後の段落で説明したように、または他の方法ですか?
ヨナ

1
申し訳ありませんが、「モックが含まれていない」という部分については明確にしませんでした。私はsavePersonあなたが提案したように関数にモックを使用しないことを意味しましたが、代わりにより一般的なを通じてテストしsavePeopleます の単体テストは、を直接呼び出すのではなくStore実行するように変更されるため、このためにモックは使用されません。ただし、実際のデータベースで発生するさまざまな統合の問題からコーディングの問題を分離したいので、もちろんデータベースは存在しないはずです。そのため、ここにはまだモックがあります。savePeoplesavePerson
ミシェルヘンリッヒ

21

savePeople()はユニットテストされるべきです

はい、そうすべきです。ただし、実装とは独立した方法でテスト条件を記述してください。たとえば、使用例を単体テストに変換します。

function testSavePeople() {
    myDataStore = new Store('some connection string', 'password');
    myPeople = ['Joe', 'Maggie', 'John'];
    savePeople(myDataStore, myPeople);
    assert(myDataStore.containsPerson('Joe'));
    assert(myDataStore.containsPerson('Maggie'));
    assert(myDataStore.containsPerson('John'));
}

このテストは複数のことを行います:

  • 機能の契約を検証します savePeople()
  • それはの実装を気にしません savePeople()
  • の使用例を記録します savePeople()

データストアをモック/スタブ/偽造することができることに注意してください。この場合、明示的な関数呼び出しをチェックするのではなく、操作の結果をチェックします。このようにして、私のテストは将来の変更/リファクタリングのために準備されます。

たとえば、データストアの実装saveBulkPerson()は将来メソッドを提供する可能性があります- savePeople()使用saveBulkPerson()する実装の変更は、saveBulkPerson()期待どおりに機能する限り単体テストを中断しません。そして、saveBulkPerson()どういうわけか期待どおりに動作しない場合、単体テストでそれキャッチします。

または、そのようなテストは、組み込みのforEach言語構造のテストに相当しますか?

前述のように、実装ではなく、期待される結果と関数インターフェイスのテストを試みてください(統合テストを行っている場合を除き、特定の関数呼び出しをキャッチすることが有用な場合があります)。関数を実装する方法が複数ある場合は、それらすべてが単体テストで機能するはずです。

質問の更新について:

状態の変化をテストします!例えば、生地の一部が使用されます。あなたの実装によると、使用量は主張doughに収まるpanかと主張するdoughまで使用されています。pan関数呼び出し後にがCookieを含むことをアサートします。ovenが空である/以前と同じ状態であることをアサートします。

追加のテストについては、エッジケースを確認ovenします。呼び出しの前にが空でない場合はどうなりますか?足りない場合はどうなりdoughますか?panがすでにいっぱいの場合は?

生地、パン、オーブンのオブジェクト自体から、これらのテストに必要なすべてのデータを推測できるはずです。関数呼び出しをキャプチャする必要はありません。その実装が利用できないかのように関数を扱ってください!

実際、ほとんどのTDDユーザーは、実際の実装に依存しないように、関数を作成する前にテストを作成します。


最新の追加:

ユーザーが新しいアカウントを作成するとき、いくつかのことを行う必要があります:1)新しいユーザーレコードをデータベースに作成する必要がある2)ウェルカムメールを送信する必要がある3)ユーザーのIPアドレスを詐欺のために記録する必要がある目的。

そこで、すべての「新規ユーザー」ステップを結び付けるメソッドを作成します。

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

このような関数の場合、(より一般的と思われるものは何でも)dataStoreおよびemailServiceパラメーターをモック/スタブ/偽造します。この関数は、それ自体ではパラメーターの状態遷移を行わず、それらの一部のメソッドにそれらを委任します。関数の呼び出しが4つのことを行ったことを確認しようとします。

  • データストアにユーザーを挿入しました
  • ウェルカムメールを送信した(または少なくとも対応するメソッドを呼び出した)
  • ユーザーのIPをデータストアに記録しました
  • 発生した例外/エラー(ある場合)を委任した

最初の3つのチェックはモック、スタブや偽物ので行うことができますdataStoreし、emailService(あなたが本当にテストしたときに電子メールを送信する必要はありません)。コメントのいくつかについてこれを調べなければならなかったので、これらは違いです:

  • 偽物とは、オリジナルと同じように動作し、ある程度区別できないオブジェクトです。通常、そのコードはテスト全体で再利用できます。これは、たとえば、データベースラッパーの単純なメモリ内データベースにすることができます。
  • スタブは、このテストの必要な操作を満たすために必要なだけ実装します。ほとんどの場合、スタブは、元のメソッドの小さなセットのみを必要とするテストまたはテストのグループに固有です。この例では、とdataStoreの適切なバージョンを実装するだけの場合がinsertUserRecord()ありrecordIpAddress()ます。
  • モックは、使用方法を確認できるオブジェクトです(ほとんどの場合、メソッドの呼び出しを評価できます)。それらを使用することで、実際に機能の実装をテストし、そのインターフェイスの順守をテストしようとしないので、ユニットテストでそれらを控えめに使用しようとしますが、それらはまだ用途があります。必要なモックだけを作成するのに役立つ多くのモックフレームワークが存在します。

これらのメソッドのいずれかがエラーをスローした場合、エラーを適切な方法で処理できるように、呼び出し元のコードまでエラーをバブルアップする必要があることに注意してください。APIコードによって呼び出されている場合、エラーを適切なHTTP応答コードに変換する場合があります。Webインターフェイスによって呼び出されている場合、エラーを適切なメッセージに変換して、ユーザーに表示するなどのことができます。重要なのは、この関数はスローされる可能性のあるエラーを処理する方法を知らないということです。

予期される例外/エラーは有効なテストケースです。このようなイベントが発生した場合、関数は期待どおりに動作することを確認します。これは、対応するモック/フェイク/スタブオブジェクトを必要に応じてスローさせることで実現できます。

私の混乱の本質は、そのような関数を単体テストするには、テスト自体で正確な実装を繰り返す必要があるように見えることです(メソッドが特定の順序でモックで呼び出されるように指定することによって)、それは間違っているようです。

場合によっては、これを実行する必要があります(ただし、統合テストではほとんどの場合これを気にします)。多くの場合、予想される副作用/状態の変化を確認する他の方法があります。

正確な関数呼び出しを検証すると、かなり脆弱な単体テストが行​​われます。元の関数にわずかな変更を加えるだけで、それらが失敗します。これは望ましい場合もそうでない場合もありますが、関数を変更するたびに対応する単体テストを変更する必要があります(リファクタリング、最適化、バグ修正など)。

悲しいことに、その場合、単体テストは信頼性の一部を失います。変更されたため、変更後の機能は以前と同じように動作しません。

例として、oven.preheat()Cookieベイキングの例で(最適化!)への呼び出しを追加する人を考えてみましょう。

  • オーブンオブジェクトをモックした場合、メソッドの観察可能な動作は変更されませんが、その呼び出しはテストに失敗し、テストは失敗しません(おそらく、クッキーのパンが残っています)。
  • スタブは、テストするメソッドのみを追加したか、ダミーメソッドを含むインターフェイス全体を追加したかによって、失敗する場合と失敗しない場合があります。
  • 偽物はメソッドを実装する必要があるため、失敗することはありません(インターフェースによる)

単体テストでは、可能な限り一般的になるようにしています。実装が変更されても、(呼び出し元の観点から)目に見える動作が同じ場合、テストに合格する必要があります。理想的には、既存の単体テストを変更する必要がある唯一のケースは、(テスト中の機能ではなく、テストの)バグ修正でなければなりません。


1
問題は、書くとすぐにmyDataStore.containsPerson('Joe')機能テストデータベースの存在を想定していることです。それができたら、単体テストではなく統合テストを作成します。
ジョナ

テストデータストアを持っていることに頼ることができると仮定し(それが実際のものであるか、または模擬のものであるかは気にしません)、すべてが設定どおりに動作することを前提としています(これらのケースのユニットテストは既にあるはずです)。テストがテストする唯一のことは、savePeople()そのデータストアが期待されるインターフェイスを実装している限り、実際にそれらの人々を提供するデータストアに追加することです。統合テストは、たとえば、データベースラッパーが実際にメソッド呼び出しに対して適切なデータベース呼び出しを行うかどうかを確認することです。
hoffmale

明確にするために、モックを使用している場合、できることは、そのモックのメソッドがおそらく特定のパラメーターで呼び出されたことをアサートすることだけです。その後、モックの状態を主張することはできません。したがって、のようにmyDataStore.containsPerson('Joe')、テスト対象のメソッドを呼び出した後にデータベースの状態についてアサーションを作成する場合は、何らかの機能的なdbを使用する必要があります。そのステップを踏むと、ユニットテストではなくなります。
ジョナ

1
実際のデータベースである必要はありません-実際のデータストアと同じインターフェイスを実装するオブジェクトだけです(読み取り:データストアインターフェイスの関連する単体テストに合格します)。私はまだこれをモックだと思っています。モックが追加するすべてのものを配列に保存し、テストデータ(の要素myPeople)が配列内にあるかどうかを確認します。私見では、モックはまだ実際のオブジェクトから期待されるのと同じ観察可能な動作を持っている必要があります。
hoffmale

「任意の方法で追加されたすべてをモックに配列に保存し、テストデータ(myPeopleの要素)が配列内にあるかどうかを確認します」-これは、まだ「アドホック」な「実際の」データベースです。作成したメモリ内のもの。「私見モックは、実際のオブジェクトから期待されるのと同じ観察可能な振る舞いをまだ持っているべきです」-私はあなたがそれを主張することができると思いますが、それは「モック」がテスト文学または私が使っている人気のあるライブラリの意味ではありません見たことある。モックは、予想されるメソッドが予想されるパラメーターで呼び出されることを確認するだけです。
ジョナ

13

このようなテストが提供する主な価値は、実装をリファクタリング可能にすることです。

私はこれまで多くのパフォーマンスの最適化をキャリアで行ってきましたが、多くの場合、あなたが示した正確なパターンに関する問題を発見しました。通常、単一のステートメントを使用して一括挿入を行う方が効率的です。

一方、私たちも時期尚早に最適化したくありません。通常、一度に1〜3人しか保存しない場合、最適化されたバッチを記述するのはやり過ぎかもしれません。

適切な単体テストを使用すると、上記の実装方法で記述できます。最適化が必要な場合は、自動テストのセーフティネットを使用してエラーをキャッチできます。当然、これはテストの品質に基づいて変化するため、十分にテストして十分にテストしてください。

この動作を単体テストすることの2番目の利点は、その目的が文書化されることです。この些細な例は明白かもしれませんが、以下の次の点を考えると、非常に重要です。

他の人が指摘している3番目の利点は、統合テストや受け入れテストではテストが非常に困難な詳細をテストできることです。たとえば、すべてのユーザーをアトミックに保存する必要がある場合は、そのためのテストケースを記述できます。これにより、期待どおりに動作することを知る方法が得られます。新しい開発者に。

TDDインストラクターから得た考えを追加します。メソッドをテストしないでください。動作をテストします。つまり、機能するかどうかをテストするのではなくsavePeople、1回の呼び出しで複数のユーザーを保存できることをテストしています。

彼らは、コードの単位がないことを確認し、私は品質のユニットテストを行うには自分の能力を発見し、私はプログラムが動作することを確認すると、ユニットテストについて考えて停止したときにTDDを向上させるのではなく、私が期待するもの。それらは異なります。彼らはそれが機能することを検証しませんが、彼らが私が思うように動作することを検証します。そのように考え始めたとき、私の視点が変わりました。


一括挿入リファクタリングの例は良い例です。ただし、OPで提案した可能性のある単体テスト(savePersonリスト内の各ユーザーに対してdataStoreのモックが呼び出した)は、一括挿入リファクタリングで壊れます。私にとって、それは貧弱な単体テストであることを示しています。ただし、実際のテストデータベースを使用して、それに対してアサートすることなく、一括実装と1人あたり1つの保存の両方を渡す代替案はありません。これは間違っているようです。両方の実装で機能するテストを提供できますか?
ジョナ

1
@ jpmc26人々が救われたことをテストするテストはどうですか...?
user253751

1
@immibis意味がわかりません。おそらく、実際のストアはデータベースによって支えられているため、単体テストのためにそれをモックまたはスタブする必要があります。そのため、その時点で、モックまたはスタブがオブジェクトを格納できることをテストします。それはまったく役に立たない。できる限り最善のsavePerson方法は、各入力に対してメソッドが呼び出されたことをアサートすることです。ループを一括挿入に置き換えた場合、そのメソッドはもう呼び出されなくなります。したがって、テストは失敗します。あなたが何か他のことを考えているなら、私はそれを受け入れますが、私はまだそれを見ません。(そして、それは私のポイントでした。)
jpmc26

1
@immibis有用なテストとは思わない。偽のデータストアを使用しても、それが本物で機能するという自信は得られません。偽物が本物のように機能することをどのように確認できますか?統合テストのスイートでそれをカバーしたいだけです。(おそらく、ここでの最初のコメントで「任意のユニットテスト」を意味していたことを明確にする必要があります。)
jpmc26

1
@immibis実際に自分の立場を再考しています。私はこのような問題のために単体テストの価値について懐疑的でしたが、入力を偽造しても価値を過小評価しているのかもしれません。私はない、入力/出力のテストをする傾向があることを知っている多くの、より便利モック重いテストよりも、多分偽物で入力を置き換えるために拒否は、実際にここでの問題の一部です。
jpmc26

6

bakeCookies()テストする必要がありますか?はい。

このような機能は、どのように単体テストする必要がありますか?生地、パン、オーブンを単純にモックしない単体テストを想像し、メソッドが呼び出されると断言するのは私にとって難しいです。

あんまり。関数が何をすべきかを注意深く見てください- ovenオブジェクトを特定の状態に設定することになっています。コードを見ると、pandoughオブジェクトの状態はあまり重要ではないようです。したがって、ovenオブジェクトを渡す(またはモックする)必要があり、関数呼び出しの最後に特定の状態にあることをアサートする必要があります。

言い換えれば、クッキーbakeCookies()焼いたと断言する必要があります

非常に短い機能の場合、単体テストはトートロジーにすぎないように見える場合があります。しかし、忘れないでください、あなたのプログラムはあなたがそれを書いて雇用されている時間よりもずっと長く続くでしょう。その機能は将来変更される場合と変更されない場合があります。

単体テストには2つの機能があります。

  1. すべてが機能することをテストします。これは、ユニットテストが最も有用でない機能であり、質問をするときにこの機能のみを検討しているようです。

  2. プログラムの将来の変更が、以前に実装された機能を壊さないことを確認します。これは単体テストの最も便利な機能であり、大きなプログラムへのバグの侵入を防ぎます。プログラムに機能を追加するときの通常のコーディングでは役立ちますが、プログラムを実装するコアアルゴリズムがプログラムの観察可能な動作を変更せずに劇的に変更されるリファクタリングおよび最適化ではより便利です。

関数内のコードをテストしないでください。代わりに、関数が実行することをテストします。この方法で単体テスト(コードではなく関数をテストする)を見ると、言語構造やアプリケーションロジックさえもテストしていないことに気付くでしょう。APIをテストしています。


こんにちは、返信ありがとうございます。2回目の更新を見て、その例で関数を単体テストする方法について考えてみてください。
ジョナ

これは、実際のオーブンまたはフェイクオーブンを使用できる場合に効果的ですが、モックオーブンでは大幅に効果が低下することがわかります。(Meszaros定義による)モックには、呼び出されたメソッドの記録以外の状態はありません。私の経験ではbakeCookies、このような方法で関数をテストすると、アプリケーションの観察可能な動作に影響を与えないリファクタリング中に壊れる傾向があります。
James_pic

@James_pic、正確に。そして、はい、それは私が使用している模擬定義です。あなたのコメントを与えられて、あなたはこのようなケースで何をしますか?テストを控えますか?とにかく、実装を繰り返す脆弱なテストを作成しますか?他に何か?
ジョナ

@Jonah私は通常、統合テストでコンポーネントをテストするか(おそらく最新のツールの品質のために、デバッグするのが難しいという警告が誇張されていることがわかりました)、または半説得力のある偽物。
James_pic

3

savePeople()は単体テストする必要がありますか、またはそのようなテストは組み込みのforEach言語構造のテストに相当しますか?

はい。しかし、コンストラクトを再テストするだけの方法でそれを行うことができます。

ここで注意すべきことは、savePerson途中で失敗したときにこの関数がどのように動作するかです。どのように動作するはずですか?

これは、単体テストで実施する必要がある、関数が提供する一種の微妙な動作です。


はい、微妙なエラー条件テストする必要があることに同意しますが、これは興味深い質問ではありません。答えは明確です。したがって、私の質問の目的のために、savePeopleエラー処理の責任を負うべきではないと具体的に述べた理由。もう一度明確にするために、それがリストを反復し、各アイテムの保存を別のメソッドに委任するだけであると仮定して、まだテストする必要がありますか?savePeople
ジョナ

@Jonah:ユニットテストを構造体のみに限定foreachし、それ以外の条件、副作用、または動作を主張しない場合、その通りです。新しいユニットテストは、それほど興味深いものではありません。
ロバートハーベイ

1
@jonah-できるだけ繰り返して保存するか、エラーで停止する必要がありますか?単一のセーブはできません、それはそれが使われている方法を知ることができないので、それを決めます。
テラスティン

1
@jonah-サイトへようこそ。Q&A形式の重要な要素の1つは、お客様をサポートするためにここにいないことです。あなたの質問はもちろんあなたを助けますが、それはまた彼らの質問に対する答えを探しているサイトに来る他の多くの人々を助けます。あなたの質問に答えました。答えが気に入らなかったり、ゴールポストを変更したりするのは私のせいではありません。そして率直に言って、より雄弁ではあるが、他の答えは同じ基本的なことを言っているように見える。
テラスティン

1
@Telastyn、ユニットテストについての洞察を得ようとしています。私の最初の質問は十分に明確ではなかったため、説明を追加して実際の質問に向けて会話を進めています。あなたはそれを私が何らかの形で「正しい」というゲームであなたをだましていると解釈することを選択しています。コードレビューとSOに関する質問に答えるのに何百時間も費やしました。私の目的は、常に私が答えている人々を助けることです。そうでない場合、それはあなたの選択です。私の質問に答える必要はありません。
ジョナ

3

ここで重要なのは、些細な特定の機能についてのあなたの見方です。プログラミングの大部分は簡単です。値を割り当て、計算を行い、決定を下します。この場合、ループは...まで続きます。プログラミング言語を教える本の最初の5章を読んだところです。

テストの作成が非常に簡単であるという事実は、設計がそれほど悪くないことを示しているはずです。テストするのが簡単ではない設計を好むでしょうか?

「それは決して変わらない。」失敗したプロジェクトのほとんどがどのように始まるかです。単体テストでは、特定の状況下でユニットが期待どおりに動作するかどうかのみを判断します。それを渡して取得すると、その実装の詳細を忘れて使用することができます。次のタスクにその脳空間を使用します。

物事が期待どおりに機能することを知ることは非常に重要であり、大規模なプロジェクト、特に大規模なチームでは簡単ではありません。プログラマに共通することが1つあるとすれば、それは私たち全員が他の誰かのひどいコードに対処しなければならなかったという事実です。最低限できることは、いくつかのテストを行うことです。疑わしい場合は、テストを作成して次に進んでください。


フィードバックありがとうございます。良い点。私が本当に答えたい質問(明確にするためにさらに別の更新を追加しました)は、委任を通じて他のサービスのシーケンスを呼び出すだけの機能をテストする適切な方法です。そのような場合、「契約の文書化」に適した単体テストは、関数実装の単なる修正であり、さまざまなモックでメソッドが呼び出されると主張しているようです。しかし、そのような場合には、実装と同一であるテストは間違って感じている....
ヨナ

1

savePeople()は単体テストする必要がありますか、またはそのようなテストは組み込みのforEach言語構造のテストに相当しますか?

これはすでに@BryanOakleyによって回答されていますが、いくつかの追加の引数があります(推測):

まず、ユニットテストは、APIの実装ではなく、契約の履行をテストするためのものです。テストでは、前提条件を設定してから呼び出し、効果、副作用、不変条件、および事後条件を確認する必要があります。何をテストするかを決めるとき、APIの実装は重要ではありません(重要ではありません)

次に、関数が変更されたときに不変条件を確認するためのテストがあります現在変更されいないという事実は、テストを受けるべきではないという意味ではありません。

第三に、TDDアプローチ(必須)とそれ以外の両方で、簡単なテストを実装したことには価値があります。

クラス用にC ++を作成するとき、オブジェクトをインスタンス化し、不変条件(割り当て可能、通常など)をチェックする簡単なテストを作成する傾向があります。開発中にこのテストが何回壊れているか(たとえば、誤ってクラスに移動できないメンバーを追加することによって)驚くことに気付きました。


1

あなたの質問は次のように要約されると思います:

統合テストではなくvoid関数を単体テストするにはどうすればよいですか?

たとえば、Cookieを返すようにCookieベーキング関数を変更すると、テストの内容がすぐに明らかになります。

関数を呼び出した後にpan.GetCookiesを呼び出さなければならない場合でも、「本当に統合テスト」なのか「単にpanオブジェクトをテストしただけなのか?」

あなたは、すべてがモックされたユニットテストを持ち、関数xyとzをチェックするだけが欠乏と呼ばれているという点で正しいと思います。

しかし!これらの場合、void関数をリファクタリングしてテスト可能な結果を​​返すか、実際のオブジェクトを使用して統合テストを行う必要があると主張します

--- createNewUserの例の更新

  • 新しいユーザーレコードをデータベースに作成する必要があります
  • ようこそメールを送信する必要があります
  • ユーザーのIPアドレスは詐欺目的で記録する必要があります。

今度は、関数の結果が簡単に返されないようにします。パラメータの状態を変更します。

これは私がわずかに物議を醸すところです。 ステートフルパラメータの具体的なモック実装を作成します

読者の皆様、あなたの怒りを試してみてください!

そう...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

これにより、テスト対象のメソッドの実装の詳細と目的の動作が分離されます。別の実装:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

単体テストに引き続き合格します。さらに、テスト全体でモックオブジェクトを再利用でき、UIまたは統合テスト用にアプリケーションにモックオブジェクトを注入できるという利点もあります。


単に2つの具体的なクラスの名前に言及しているため、統合テストではありません...統合テストは、ディスクIO、DB、外部Webサービスなどの外部システムとの統合をテストすることに関するものです。 -メモリ、高速、興味のあることなどをチェックします。メソッドにCookieを直接返すことは、より良い設計のように感じることに同意します。
サラ

3
待つ。すべてのために我々はpan.getcookiesは、彼らはチャンスを得るときオーブンからクッキーを取るためにそれらを求めて調理する電子メールを送信知っている
ユアン・

理論的には可能ですが、それはかなり誤解を招く名前になるでしょう。電子メールを送信したオーブン機器を聞いたことがありますか?しかし、私はあなたが指摘するのを見ます、それは依存します。これらのコラボレーションクラスはリーフオブジェクトまたは単なるメモリ内のものであると想定しますが、こっそりと行う場合は注意が必要です。ただし、メールの送信はこれよりも高いレベルで行う必要があります。これは、エンティティのダウンした汚いビジネスロジックのようです。
サラ

2
それは修辞的な質問でしたが、「電子メールを送信するオーブン機器を聞いたことがありますか?」 venturebeat.com/2016/03/08/...
clacke

こんにちはユアン、この答えは私が本当に求めているものに最も近いと思います。bakeCookies焼きたてのクッキーを返すことについてのあなたの論点は正しかったと思います、そして、私はそれを投稿した後にいくらか考えました。だから私はそれが再び良い例ではないと思います。質問の動機となるもののより現実的な例を提供することを願って、もう1つの更新を追加しました。ご意見をお寄せください。
ジョナ

0

また、テストする必要がありますbakeCookies-何がbakeCookies(egg, pan, oven)得られますか?目玉焼きか例外か?それらはどれも想定されていないため、それ自体では、実際の成分を気にしませpanovenが、bakeCookies通常はクッキーを生成する必要があります。より一般的にはそれがどのように依存することができdough得、そこにあるか否か、それは単なるになっての任意のチャンスeggまたは例えばwaterその代わりに。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.