web-dev-qa-db-ja.com

TDDが役に立たなかったときに、コードの論理的な間違いを回避する方法は?

私は最近、イベントがどれくらい古いかを人に優しい方法で示す小さなコードを書いていました。たとえば、イベントが「3週間前」または「1か月前」または「昨日」に発生したことを示している可能性があります。

要件は比較的明確で、これはテスト駆動開発の完璧なケースでした。私はテストを1つずつ作成し、各テストに合格するためのコードを実装しましたが、すべてが完全に機能しているように見えました。本番環境でバグが発生するまで。

関連するコードは次のとおりです。

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return _number_to_text(delta) + " days ago"

if delta < 30:
    weeks = math.floor(delta / 7)
    if weeks == 1:
        return "A week ago"

    return _number_to_text(weeks) + " weeks ago"

if delta < 365:
    ... # Handle months and years in similar manner.

テストでは、今日、昨日、4日前、2週間前、1週間前などに発生するイベントのケースをチェックし、それに応じてコードをビルドしました。

私が見逃したのは、1日前のイベントが1日前に発生する可能性があることです。たとえば、26時間前に発生したイベントは1日前ですが、今が午前1時の場合は正確に昨日ではありませんより正確には、1ポイントです。何か、しかしdeltaは整数なので、それは1だけです。この場合、アプリケーションは「1日前」を表示します。これは明らかに予期しないものであり、コードでは処理されません。以下を追加することで修正できます:

if delta == 1:
    return "A day ago"

deltaを計算した直後。

バグの唯一の負の結果は、このケースがどのように発生するのか(そしてコードでUTCが均一に使用されているにもかかわらず、タイムゾーンに関係していると考えている)と思って30分無駄にしたことですが、その存在は私を悩ませています。次のことを示しています。

  • このような単純なソースコードであっても、論理的な間違いを犯すことは非常に簡単です。
  • テスト駆動開発は役に立ちませんでした。

また、このようなバグをどのようにして回避できるのかわかりません。コードを書く前にもっと考えることを除いて、私が考えることができる唯一の方法は、私が絶対に起こらないと信じるケースにたくさんのアサートを追加することです(私は1日前は必然的に昨日だと信じていたように)、そして毎秒ループして過去10年間、アサーション違反をチェックしています。これは複雑すぎるようです。

最初にこのバグを作成しないようにするにはどうすればよいですか?

67

これらは、red/green/refactorのrefactorステップで通常見られる種類のエラーです。そのステップを忘れないでください!次のようなリファクタリングを検討してください(テストされていません):

def pluralize(num, unit):
    if num == 1:
        return unit
    else:
        return unit + "s"

def convert_to_unit(delta, unit):
    factor = 1
    if unit == "week":
        factor = 7 
    Elif unit == "month":
        factor = 30
    Elif unit == "year":
        factor = 365
    return delta // factor

def best_unit(delta):
    if delta < 7:
        return "day"
    Elif delta < 30:
        return "week"
    Elif delta < 365:
        return "month"
    else:
        return "year"

def human_friendly(event_date):
    date = event_date.date()
    today = now.date()
    yesterday = today - datetime.timedelta(1)
    if date == today:
        return "Today"
    Elif date == yesterday:
        return "Yesterday"
    else:
        delta = (now - event_date).days
        unit = best_unit(delta)
        converted = convert_to_unit(delta, unit)
        pluralized = pluralize(converted, unit)
        return "{} {} ago".format(converted, pluralized)

ここでは、抽象度の低い3つの関数を作成しました。これらの関数は、まとまりがあり、分離してテストするのが簡単です。意図した期間を省略した場合、単純なヘルパー関数では親指のように突出します。また、重複を取り除くことで、エラーの可能性を減らします。壊れたケースを実装するには、実際にはaddコードを使用する必要があります。

このようなリファクタリングされたフォームを見ると、他のより微妙なテストケースもすぐに思い浮かびます。たとえば、best_unitdeltaが負の場合はどうしますか?

言い換えれば、リファクタリングはそれをきれいにするだけではありません。コンパイラが検出できないエラーを人間が見つけやすくなります。

57
Karl Bielefeldt

テスト駆動開発は役に立ちませんでした。

それは助けになったようです、それはあなたが「1日前」のシナリオのテストを持っていなかったということだけです。おそらく、このケースが見つかった後にテストを追加しました。これはまだTDDであり、バグが見つかった場合は、単体テストを記述してバグを検出し、修正します。

振る舞いのテストを書くのを忘れた場合、TDDは何の助けにもなりません。テストを書くのを忘れているので、実装を書かないでください。

149
esoterik

26時間前に起こったイベントは1日前です

問題の定義が不十分な場合、テストはあまり役に立ちません。明らかに、カレンダーの日と時間で計算された日が混在しています。暦日に固執する場合、午前1時の26時間前はnot昨日です。また、時間に固執する場合、26時間前は時間に関係なく1日前に丸められます。

114
Kevin Krumwiede

できません。 TDDは、認識している可能性のある問題からユーザーを保護するのに最適です。考えたことのない問題に遭遇した場合は役に立ちません。あなたの最善の策は、誰かがシステムをテストすることです、彼らはあなたが考えたことのないEdgeのケースを見つけるかもしれません。

関連資料: 大規模なソフトウェアでバグの状態を完全にゼロにすることは可能ですか?

38
Ian Jacobs

私が助けることができると私が見つける通常私がとる2つのアプローチがあります。

まず、Edgeのケースを探します。これらは、動作が変化する場所です。あなたのケースでは、一連の正の整数日に沿っていくつかの点で動作が変化します。 0、1、7などにEdgeケースがあります。次に、Edgeケースとその周辺にテストケースを記述します。私は-1日、0日、1時間、23時間、24時間、25時間、6日、7日、8日などでテストケースを持っています。

2番目に探すのは、行動のパターンです。数週間のロジックでは、1週間の特別な処理があります。おそらく、表示されていない他の各間隔にも同様のロジックがあります。このロジックはnotですが、数日間存在します。そのケースが異なる理由を検証可能に説明できるようになるか、ロジックを追加するまで、私はそれを疑いを持って見ていきます。

35
cbojar

できない TDDの要件に存在する論理エラーをキャッチします。しかし、それでもTDDは役立ちます。結局、エラーが見つかり、テストケースが追加されました。しかし基本的に、TDD onlyは、コードがメンタルモデルに確実に準拠するようにします。あなたのメンタルモデルに欠陥がある場合、テストケースはそれらをキャッチしません。

ただし、バグを修正している間、既存のテストケースでは、機能している既存の動作が壊れていないことを確認してください。これは非常に重要です。1つのバグを修正することは簡単ですが、別のバグを導入することは簡単です。

これらのエラーを事前に見つけるために、通常、等価クラスベースのテストケースを使用しようとします。その原則を使用して、すべての等価クラスから1つのケースを選択し、次にすべてのEdgeケースを選択します。

各等価クラスの例として、今日、昨日、数日前、正確に1週間前、数週間前の日付を選択します。日付をテストする場合、テストでシステムの日付を使用することも確認してくださいnotが、比較のために事前に決定された日付を使用します。これにより、いくつかのEdgeケースも強調されます。テストは1日の任意の時間に実行する必要があります。真夜中の直後、真夜中の直前、さらには直接at真夜中にもテストを実行します。これは、各テストに対して、テスト対象の4つの基本時間があることを意味します。

次に、他のすべてのクラスに体系的にEdgeケースを追加します。今日のテストがあります。したがって、動作が切り替わる直前と直後に時間を追加します。昨日も同じ。 1週間前も同様。

おそらく、すべてのEdgeケースを体系的に列挙し、それらのテストケースを書き留めることにより、仕様に詳細が欠けていることがわかり、それを追加します。日付を処理することは、しばしば間違ったものになることに注意してください。なぜなら、人々は、異なる時間で実行できるようにテストを書くことを忘れがちだからです。

ただし、私が書いたことのほとんどはTDDとはほとんど関係がないことに注意してください。等価クラスを書き留め、独自の仕様がそれらについて十分詳細に記述されていることを確認することについてです。 Thatは、論理エラーを最小限に抑えるプロセスです。 TDDは、コードがメンタルモデルに準拠していることを確認するだけです。

テストケースを思いつくのはhardです。等価クラスに基づくテストはそれだけではありません。場合によっては、テストケースの数を大幅に増やすことができます。現実の世界では、allを追加することは、多くの場合、経済的に実行可能ではありません(理論的には実行する必要があります)。

14
Polygnome

私が考えることができる唯一の方法は、私が絶対に起こらないと信じているケースにたくさんのアサートを追加することです(私は1日前は必然的に昨日だと信じていたように)、そして過去10年間毎秒ループしてチェックします非常に複雑に見えるアサーション違反。

何故なの?これはかなり良い考えのように思えます!

コードにコントラクト(アサーション)を追加することは、コードの正確性を向上させるかなり確かな方法です。一般に、関数のエントリではpreconditionsとして、関数の戻りではpostconditionsとして追加します。たとえば、すべての戻り値が[] "A [unit] ago"または "[number] [unit] s ago"の形式であるという後置条件を追加できます。規則正しい方法で行うと、これは契約による設計につながり、高保証コードを記述する最も一般的な方法の1つです。

重要なこととして、契約はテストされることを意図していません。テストと同じくらいコードの仕様です。ただし、テストは可能ですviaコントラクト:テストでコードを呼び出し、どのコントラクトでもエラーが発生しない場合、テストに合格します。 every過去10年間の2番目をループするのは少し多いです。ただし、プロパティベースのテストと呼ばれる別のテストスタイルを利用できます。

PBTでは、コードの特定の出力をテストする代わりに、出力がいくつかのプロパティに従うことをテストします。たとえば、reverse()関数の1つのプロパティは、任意のリストlreverse(reverse(l)) = lのプロパティです。このようなテストを作成する利点は、PBTエンジンに数百の任意のリスト(およびいくつかの病理学的リスト)を生成させ、それらすべてにこのプロパティがあることを確認できることです。 しないがある場合、エンジンは失敗したケースを「縮小」して、コードを破壊する最小限のリストを見つけます。 Hypothesis をメインのPBTフレームワークとして持つPythonを書いているようです。

したがって、考えられないかもしれないよりトリッキーなEdgeケースを見つけるための良い方法が必要な場合は、コントラクトとプロパティベースのテストを一緒に使用すると、非常に役立ちます。もちろん、これは単体テストの記述に取って代わるものではありませんが、それを補強するものであり、エンジニアとしてできる最高のことです。

12
Hovercouch

これは、モジュール性を少し追加することが役立つ例です。エラーが発生しやすいコードセグメントが複数回使用されている場合は、可能であればそれを関数にラップすることをお勧めします。

def time_ago(delta, unit):
    delta_str = _number_to_text(delta) + " " + unit;
    if delta == 1:
        return delta_str + " ago"
    else:
        return delta_str = "s ago"

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return time_ago(delta, "day")

if delta < 30:
    weeks = math.floor(delta / 7)
    return time_ago(weeks, "week")

if delta < 365:
    months = math.floor(delta / 31)
    return time_ago(months, "month")
5
Antonio Perez

テスト駆動開発は役に立ちませんでした。

TDDは、テストを作成する人が敵対的である場合のテクニックとして最適です。ペアプログラミングでない場合、これは難しいので、これについて考える別の方法は次のとおりです。

  • テスト中の機能が作成したとおりに機能することを確認するためのテストを記述しないでください。意図的にそれを壊すテストを書いてください。

これは、TDDの有無にかかわらず正しいコードの記述に適用される別の技術であり、実際にコードを記述するよりも(そうでない場合でも)おそらく複雑です。それはあなたが練習する必要があるものであり、そのための単一で簡単で単純な答えはありません。

堅牢なソフトウェアを作成するためのコアテクニックは、効果的なテストを作成する方法を理解するためのコアテクニックでもあります。

関数の前提条件-有効な状態(つまり、関数がメソッドであるクラスの状態についてどのような仮定をしているのか)と有効な入力パラメーターの範囲-各データ型には、可能な値の範囲-のサブセットがあります。関数によって処理されます。

関数の入力時にこれらの仮定を明示的にテストするだけで、違反がログに記録またはスローされたり、関数がエラーなしで処理されることを確認したりする場合は、ソフトウェアが本番環境で失敗しているかどうかをすばやく確認できます。エラー耐性があり、敵対的なテストライティングスキルを開発します。


NB。事前条件と事後条件、不変条件などに関する全資料と、属性を使用してそれらを適用できるライブラリがあります。個人的には、私はそれほど正式に行くのは好きではありませんが、調べる価値はあります。

5
Chris Becke

これはソフトウェア開発に関する最も重要な事実の1つです。バグのないコードを書くことは絶対に不可能です。

TDDは、あなたが考えていなかったテストケースに対応するバグを導入することからあなたを救うことはありません。また、それを実現せずに誤ったテストを記述してから、たまたまバグのあるテストに合格する誤ったコードを記述しないようにすることもできません。そして、これまでに作成された他のすべての単一のソフトウェア開発手法には、同様の穴があります。開発者として、私たちは不完全な人間です。結局のところ、100%バグのないコードを書く方法はありません。それは決して起こらず、決して起こらないでしょう。

これはあなたが希望を放棄すべきだと言っているのではありません。完全に完璧なコードを書くことは不可能ですが、非常にまれなEdgeのケースに現れる非常に少ないバグがあり、ソフトウェアの使用が非常に実用的であるコードを書くことは非常に可能です。バグのある動作を示さないソフトウェア実際にはを書くことは非常に可能です。

しかし、それを書くには、バグのあるソフトウェアを作成するという事実を受け入れる必要があります。ほぼすべての最新のソフトウェア開発手法は、最初の段階でバグが発生するのを防ぐか、必然的に発生するバグの影響から自分自身を保護することを中心に構築されています。

  • 徹底した要件を収集することで、コード内でどのような不正な動作が発生するかを知ることができます。
  • クリーンで注意深く設計されたコードを記述することで、最初からバグが発生するのを回避しやすくなり、バグを特定したときに修正しやすくなります。
  • テストを作成することで、ソフトウェアで発生する可能性のある最悪のバグの多くが何であるかを記録し、少なくともそれらのバグを回避できることを証明できます。 TDDはコードの前にそれらのテストを生成し、BDDは要件からそれらのテストを導き出し、旧式のユニットテストはコードが記述された後にテストを生成しますが、すべてが将来の最悪のリグレッションを防ぎます。
  • ピアレビューは、コードが変更されるたびに、少なくとも2組の目がコードを見ていることを意味し、バグがマスターに侵入する頻度を減らします。
  • バグをユーザーストーリーとして扱うバグトラッカーまたはユーザーストーリートラッカーを使用すると、バグが発生したときに追跡され、最終的に対処されます。
  • ステージングサーバーを使用するということは、メジャーリリースの前に、show-stopperのバグが発生して対処できる可能性があるということです。
  • バージョン管理を使用するということは、重大なバグのあるコードが顧客に出荷されるという最悪のシナリオでは、緊急ロールバックを実行して、信頼できる製品を顧客の手元に戻すことができるということです。

特定した問題の最終的な解決策は、バグのないコードを作成することを保証できないという事実と戦うことではなく、それを受け入れることです。開発プロセスのすべての領域で業界のベストプラクティスを採用します。完全ではありませんが、ジョブに対して十分に堅牢なコードをユーザーに一貫して提供します。

1
Kevin

あなたは単に前にこのケースについて考えたことがないので、それのためのテストケースを持っていませんでした。

これは常に発生し、正常です。すべての可能なテストケースを作成するのにどれだけの労力を費やすかは、常にトレードオフです。すべてのテストケースを検討するために無限の時間を費やすことができます。

航空機の自動操縦では、単純なツールよりもはるかに多くの時間を費やします。

多くの場合、入力変数の有効範囲を検討し、これらの境界をテストすると役立ちます。

さらに、テスターが開発者とは別の人物である場合、多くの場合、より重要なケースが見つかります。

1
Simon

(そして、コードでのUTCの統一的な使用にもかかわらず、それはタイムゾーンに関係していると信じています)

これは、ユニットテストをまだ行っていない、コードのもう1つの論理的な誤りです。計算する前に、「今」とイベントの日付の両方をユーザーのローカルタイムゾーンに変換する必要があります。

例:オーストラリアでは、イベントは現地時間の午前9時に発生します。午前11時にUTC日付が変更されたため、「昨日」と表示されます。

1
Sergey
  • 他の誰かにテストを書かせてください。この方法で、実装に不慣れな誰かが、あなたが考えていないまれな状況をチェックするかもしれません。

  • 可能であれば、テストケースをコレクションとして挿入します。これにより、yield return new TestCase(...)のような別の行を追加するのと同じくらい簡単に別のテストを追加できます。これは 探索的テスト の方向に進むことができ、テストケースの作成を自動化します。

0
null

すべてのテストに合格した場合、バグはないと誤解されているようです。実際には、すべてのテストに合格すると、すべてのknownの動作が正しくなります。未知の動作が正しいかどうかはまだわかりません。

うまくいけば、TDDでコードカバレッジを使用しています。予期しない動作の新しいテストを追加します。次に、予期しない動作のテストだけを実行して、コードが実際に通過するパスを確認できます。現在の動作がわかったら、修正して変更を加えることができます。すべてのテストに再び合格すると、正しく動作したことがわかります。

これは、コードにバグがないことを意味するのではなく、以前よりも優れているということです。そして、もう一度既知の動作がすべて正しいです!

TDDを正しく使用しても、バグのないコードを作成できるとは限りません。つまり、作成するバグが少なくなるということです。あなたは言う:

要件は比較的明確でした

これは、1日以上ではなく昨日ではない動作が要件で指定されたことを意味しますか?書面での要件を満たしていない場合、それはあなたの責任です。コーディングしているときに要件が不完全であることに気付いた場合、それはあなたにとって良いことです!要件に取り組んだすべての人がそのケースを逃した場合、あなたは他の人よりも悪いことではありません。誰もが間違いを犯し、微妙なほど、ミスを犯しやすくなります。ここで重要なのは、TDD しない防止するすべてエラーであるということです。

0
CJ Dennis

このような単純なソースコードであっても、論理的な間違いを犯すことは非常に簡単です。

はい。テスト駆動開発はそれを変更しません。実際のコードおよびテストコードにもバグを作成できます。

テスト駆動開発は役に立ちませんでした。

ああ、でもそうだった!まず、バグに気づいたときは、すでに完全なテストフレームワークが用意されており、テスト(および実際のコード)のバグを修正するだけで済みます。第2に、最初にTDDを実行しなかった場合にどれだけ多くのバグが発生するかはわかりません。

また、このようなバグをどのようにして回避できるのかわかりません。

できません。 NASAでさえバグを回避する方法を見つけていません。私たち以下の人間もそうではありません。

コードを書く前にもっと考えることはさておき、

それは誤りです。 TDDの最大の利点の1つは、less思考でコーディングできることです。これらのテストはすべて、少なくとも回帰をかなりうまくキャッチするためです。また、特にTDDの場合でも、notは最初からバグのないコードを提供することが期待されています(または開発速度が低下して停止します)。

私が考えることができる唯一の方法は、私は決して起こらないと信じているケースにたくさんのアサートを追加することです(私は1日前は必然的に昨日だと信じていたように)、そして過去10年間毎秒ループしてチェックします非常に複雑に見えるアサーション違反。

これは明らかに、現在実際に必要なものだけをコーディングするという原則と矛盾します。あなたはそれらのケースが必要だと思ったので、そうでした。これは重要ではないコードでした。あなたが言ったように、30分間それについて不思議に思っていることを除いて、損傷はありませんでした。

ミッションクリティカルなコードの場合、実際にあなたが言ったことを実行できますが、日常の標準コードでは実行できません。

最初にこのバグを作成しないようにするにはどうすればよいですか?

あなたはしません。ほとんどのリグレッションを見つけるためにテストを信頼します。赤-緑-リファクターサイクルを維持し、実際のコーディングの前/最中にテストを記述し、(重要!)赤-緑の切り替えに必要な最小限の量を実装します(多すぎず、少なすぎません)。これは素晴らしいテストカバレッジ、少なくともポジティブなものになるでしょう。

バグを見つけた場合は、そのバグを再現するためのテストを作成し、最小限の作業でバグを修正して、そのテストを赤から緑に移行させます。

0
AnoE