web-dev-qa-db-ja.com

機能の実装を簡素化する単体テストを作成するにはどうすればよいですか?

私はソフトウェア開発の初心者であり、単体テストがいかにすばらしいかについてたくさん読んでいます。さて、私は最初に一歩踏み込んで、同じように経験のないプログラマーと一緒に作業しているので、少しスパゲッティコードを作成しました。テストやその他の手法を使用してコードの品質を向上させる方法を学びたいと思っていますが、私の初心者の同僚の1人が、テストは物事をより難しくすると言っています。どうやら、彼はユニットテストが使用されたチームでインターンシップを行ってきました。

彼は新しい機能を実装しようとしたときにテストが常に邪魔になると主張しました。彼がコードを変更した後、テストは失敗しました。そのため、彼はテストを適応させる必要があり、もちろんそれによってワークロードが増加しました。

しかし、それは私には意味がありません。テストは物事を簡単にするためのものだと思いました。それで、彼が機能を正しく実装しなかったか、単体テストが正しく行われなかったのではないかと思います。それで、私は疑問に思います:新しい機能が実装されたからといって失敗しないように、単体テストをどのように記述できますか?

35
Gerome Bochmann

それらに彼らが関係していることだけをテストさせることによって、今は真実だが後で変更されるかもしれない多くの無関係なプロパティをテストしないことによって。

私の経験からのいくつかの例。多くの場合、システムは特定のことが起こったときにユーザーに通知を送信することになっています。適切なテストハーネスを使用すると、電子メールメッセージを模倣して、メッセージが送信され、正しい受信者に到達し、想定されている内容を伝えることを簡単に確認できます。ただし、「これが発生した場合でも、このユーザーはその正確なメッセージテキストを受信する」と単純に主張することはお勧めできません。このようなテストは、I18Nテキストが改訂されると必ず失敗します。 「メッセージにユーザーの新しいパスワード/アナウンスされたリソースへのリンク/ユーザーの名前と挨拶が含まれている」と断定する方がはるかに良いので、テストは機能し続け、ルーチンが実際に実行されたときにのみ中断しますジョブ。

同様に、自動生成されたIDをテストする場合、現時点で生成される値が常に生成されるとは決して想定しないでください。テストハーネスが変更されない場合でも、その機能の実装は変更される可能性があるため、契約を満たしている間に結果が変更されます。繰り返しますが、「最初のユーザーはID AAAを受け取る」と主張するのではなく、「最初のユーザーは文字で構成されるIDを受け取り、2番目のユーザーも文字で構成されるIDを受け取ります。

一般に、テストしているものとの契約に含まれていないもののテストには注意してください。ユニットの動作について本質的に trueとは何か、そして誤って trueとは何であるかを理解することは、最小限のカバーテストを作成するための鍵であり、システムを理解するのにも非常に役立ちますそしてそれを首尾よく維持します。これは、テスト駆動開発がバグをキャッチしなくても結果を改善する1つの方法です。

66
Kilian Foth

ソフトウェアを高度にテスト可能にするには、テストを念頭に置いて設計および実装する必要があります。これは、ソフトウェアが契約に従って設計され、テストがその契約を検証するために実行される、Killian Fothの回答を補足するものです。

「テスト容易性のためのコードの設計」とは、たとえば、モック、スタブなど。これらは抽象化を作成するさまざまな方法であり、ソフトウェア部品を自由に置き換えることができる "seams" です。

(単語「継ぎ目」は、Michael Feathersの本 Working Effectively With Legacy Code から借用しています。別の推奨本は xUnit Test Patterns:Refactoring Test Code のGerard Meszarosです。重いですが、それだけの価値があります。)


言語学習者は一度に多くのことを学ぶ必要があるかもしれません。そのため、「テスト容易性のためのコードの設計」により、学習者の作業負荷が増加し、最初は学習の進行が遅くなる可能性があります。

両方の方法を試すことをお勧めします。チームで作業するときは、テスト容易性(およびその他の品質属性)を考慮して設計するように十分な努力を払ってください。ただし、それぞれの人には、言語自体の学習に焦点を当てる、または何かをすばやく行う(たまにあるため)このようなニーズが発生します)、またはまったく新しい、実証されていない方法で何かを創造的に試したり、実験したりするだけです。


最後に、プログラミングとテストのための設計に十分な経験があるプログラマーは、「壊れたテスト」を好意的に見るようになります。

実装が何らかの形で変更されたためにテストが失敗した場合、プログラマがこれを予期していないときにこれが発生すると、これはプログラマが何かを学んだ瞬間です。たとえば、プログラマはコードの動作が変更されたことを予期していない可能性がありますが、テストはそれ以外のことを伝えています。これが発生すると、壊れたテストは潜在的なバグをうまく回避しました。

28
rwong

はい、そうです。単体テストを作成するとワークロードが増加します。コードを変更すると、テストが失敗し始め、テストを更新する必要があり、ワークロードがさらに増加し​​ます。

ただし...テストの影響を受けるコードを変更する場合に適用されます。通常、一部の単体テストでテストされるコード部分を記述し、それらのコード部分を再度編集することはありません。結局のところ、それらは機能し、新しい機能を追加することでそれらを編集する必要がないように正しく設計したので、ここに問題があると思います。経験の浅いコーダーは、コードベースの将来を十分に考えずにコードをハッキングする傾向があります。フィールドを含むクラスを追加した後、突然別のフィールドを追加する必要があり、元のフィールドのタイプを変更する必要があるなどの場合、このチャーンは、最初に「正しく理解する」ことを検討することで回避することを学ぶものです差し迫った問題を解決するだけでなく、コードが何をすることになっているのか、それが製品全体にどのように適合するのか。もちろん、そのような変更を行う必要がありますが、経験豊富な開発者がこれを行うことはほとんどありません。

途中で役立つと思われることの1つは、ユニットテストのスタイル red-green-refactor を使用することです。これは、デバッガーを使用してテストするのと似ています。コードを記述してからデバッガーで実行して、どのようなエラーがあるかを確認し、エラーを修正して、デバッグを繰り返します。デバッガーを使用する代わりに、代わりに自動テストを使用します。テストを記述し、コードで実行し、コードを更新するなど。秘訣は、ほんの少しのコードを書いてそれらを非常に頻繁にテストすることであり、ユニットテストはミニテストハーネスのようになります。私はこのアプローチが、(概念的には)おそらくとにかく行う手動テストと非常によく似ているため、より適切に適合すると思います。

11
gbjbaanb

私の初心者の同僚の1人は、テストによって物事がより困難になると言っています。

ここでの鍵はwhat thingsだと思います。ソフトウェア開発には多くのプロセスが含まれます。実行可能コードの記述はそのうちの1つにすぎません。

実行可能(/ compilable/etc。)コードの作成に焦点を当てる場合は、テストの作成doesを実行することにより、さらに注意が必要です。たとえば、関数の名前を変更するような重大な変更を行う場合、「実行可能コード」という目標を達成するために、テストコードも更新する必要があります。

もちろん、実行可能コードを持つことis n't最終目標:ビジネスの最終目標は通常、収益を上げることです。開発者にとって最終的な目標は通常、ビジネスの収益化計画をサポートするために、ソフトウェアのバグを減らすか、機能セット/有用性を高めることです。

ソフトウェアは非常に複雑であるため、1つのバグを修正したり、1つの機能を個別に導入したりすることはまれです。通常、私たちが行う変更は、新しいバグを導入するか、既存のバグを公開または悪化させます。新しい機能を追加すると、直接(以前の機能を実行しない)または概念的に(新しい機能を考えると意味がありません)、古い機能が壊れる可能性があります。

テストのポイントは、「ブギネス」と機能セットを追跡し、客観的に追跡することです。既存の機能が動作することをテストスイートにチェックさせることで、古いものを犠牲にして新しいものを追加するのではなく、自分の作業がincreeasing機能セットであることをある程度確信します。重要な、問題を引き起こすと予想される入力/パス(エッジケース)、または以前に問題を引き起こした(回帰テスト)テストスイートチェック入力/パスを使用することで、「ブギネス」が減少であるという自信を自分に与えます。 =、古いバグを犠牲にして新しいバグを修正するのではなく。

テストはソフトウェアエンジニアリングの一般的なパターンに従います。テストはクレームソフトウェアをより良く/より「機敏」/バグが少ないなどにするためのものですが、実際にこれを行うのはテストではありません。むしろ、優れた開発者ソフトウェアをより良い/より「機敏」にする/バグが少ない/など。およびテストは優れた開発者が行う傾向があるものです

言い換えれば、 何らかの儀式を実行する 単体テストのように、それ自体でnotコードを改善します。しかし、理解理由多くの人々がユニットテストを行います意志より良い開発者になり、より優れた開発者になります意志コードをより良くします単体テストがあるかどうか

残念ながら、できませんgit pullこれらのスキルは相互に関連しています。時間と労力がかかり、一人一人がこれらのことを自分で学ばなければなりません。あなたの同僚は、なぜ特定のことが行われるのかについての理解を深めることなく、儀式的なテストを余儀なくされたようです。

おもちゃのプロジェクトでこれらのアイデアをいじることをお勧めします。たとえば、「依存関係の注入が必要な状況では...」などの説明が表示された場合、その状況をおもちゃの例にして、発生した問題を確認し、解決してみてください次に依存性注入が何であるかを確認してみてください。そうすることで、何かが適切であることがわかり、不適切であることがわかり、問題へのさまざまなアプローチを説明し、議論することができます(他の人が主張していることを述べるだけでなく)。

6
Warbo

彼は新しい機能を実装しようとしたときにテストが常に邪魔になると主張しました。彼がコードを変更した後、テストは失敗しました。そのため、彼はテストを適応させる必要があり、もちろんそれによってワークロードが増加しました。

これは、テストが新機能の実装に役立つ一番の方法です。

彼がコードを変更した後、テストは失敗しました。

コードは吸いました。たぶんテストコードは吸いました。おそらく彼のコードは駄目だった。いずれにせよ、コードは吸いました。

彼のコードが原因となった可能性が約95%、テストコードが意図的に機能しなかった可能性が2%あるとしましょう(「ケースAは許可しない必要があります。 Bだから、誰もそれに当たらないことを確認するためにテストしている」)、テストコードが失敗する可能性は3%です。

現在は非常に寛大ですが、率が99.99%まで上がっていても、その人のスキルや職人の技に悪いことは何もありません。 (コンパイラエラーが発生する頻度を検討してください。これは、テストを実行することさえできなかったコードですが、コンパイラエラーの原因を修正した最終製品と同様に、その日の全体的な作業は素晴らしいものでした)。実際、ここでパーセンテージをヒットすることはそれほど寛大ではありません。テストコードは、頻繁に変更されないため、新しいバグを取得する機会が少ないという理由だけで、非難される頻度が低くなる傾向があります。また、ここでは障害が発生した頻度ではなく、特定された障害の場合について話していることを思い出してください。

とにかく、これらのパーセンテージでは、修正が必要な何かが不正確である可能性は98%であり、対処する必要があるコントロールの外で何かが不正確である可能性は2%であり、変更する必要がある可能性は100%です。何か。

テストが正しくない可能性が3%ある場合、さらにいくつかの可能性に分類されます。

  1. テストはバギーです。
  2. テストはもはや関係ありません。
  3. テストは特定のケースでは無効であり、一般的に適用されています。

そのため、テストが原因である場合でも、必ずしも「間違っている」とは限りません。

これらの3%のケースでは、テストを処理すると、テストを使用しなかった場合には必要のない追加の作業が発生します。しかし、その作業が完了すると、何か他のことが原因である場合の97%の時間が支援されます。

そして、97%の時間でテストにより多くの作業が必要になったその場でしかし、その結果、非テストコードはこれ以上吸わない(95%の時間)または外部の制御不能の問題を問題なく処理します(時間の2%)。 2%の障害ではない場合でも、ユーザーは「このソフトウェアは問題ない」と認識していることに注意してください。

したがって、テストによりコードの吸引力が低下した時間の97%(実際にはそれ以上)。

単体テストで欠陥を見つけずに、その97%の代替案を検討してみましょう。

  1. コードの問題はまだ残っていましたが、実際には関連するコードパスに影響を与えるものはありませんでした。 (ねえ、それは起こります)。
  2. 他のコードが機能しないときにコードの問題が見つかりました。
  3. コードの問題は、別の開発者が何かをしなければならなかったときに発見されました。
  4. コードの問題は、機能テストまたはアプリケーションテストで見つかりました。
  5. コードの問題は、ユーザー受け入れテストで見つかりました。
  6. コードの問題は、製品がリリースされた後にイライラしたユーザーによって発見されました。
  7. コードの問題は、製品がリリースされた後、喜んでクラッカーがそれを利用してあなたまたはあなたの顧客のセキュリティを危険にさらすことによって発見されました。
  8. コードの問題は見つかりませんでしたが、不正な金融取引、怪我、命の喪失、または敵の命の継続を引き起こしました。 (最後のいくつかは、ますます特殊化するアプリケーションにのみ適用され、あなたには当てはまらないかもしれませんが、確かに一部のプログラマーには当てはまります)。

さらに言えば、プロのプログラマー、そして仕事を解放するアマチュアは、これらのケースの最初の6件が発生したことになります。私たちの何人かは最後の2を経験しました。これらは私たちが最も避けようとしているシナリオですが、実際に起こります。

項目2〜5が発生すると、ワークロードへの影響は、失敗した単体テストの影響よりもはるかに大きくなります。多くの場合、開発よりもこれらの問題への対処に多くの時間が費やされます。彼らがそのような問題を乗り越えたことがないので、非常に多くのプロジェクトがアルファから抜け出すことはありません。

リストの後半の項目については、ワークロードへの影響は、それらについてより悪いことではないかもしれません。

ユニットテストが失敗した大部分の時間は、上記のシナリオの1つから救われています。単体テストはそれらすべてからあなたを救うわけではありませんが、それらの多くからあなたを救うでしょう。

合格したテストは素晴らしいです。彼らはあなたに少なくとも特定のバグが特定の単位で存在しないという自信をあなたに与えることができ、それは素晴らしいことです。

失敗したテストは、維持されたテストです。

ただし、注意すべき点が1つあります。

彼がコードを変更した後、テストは失敗しました。

失敗したテストもあるといいのですがbefore彼はコードを変更していました。新機能はまだ実装されていないため、新機能に関連するすべてのテストは失敗し始めたはずです。

3
Jon Hanna