閾値問題に関する考察
以下の文章はこちらの記事を翻訳したものです。
先日の投稿で、テスト過程におけるバグの見落としに触れました。特に、一つのバグを追いかけている最中に別のバグに気づくという状況について述べました。
私はこのような状況に二度遭遇したことがありますが、これまでその詳細については言及していませんでした。
これはテスト中に時折起こることで、問題は、重要なバグを過小評価してしまうことにあります。
この問題に最初に気づいたのは、ネッド・バチェルダーが、ある混乱を招く振る舞いについて私に質問したときです。彼は浮動小数点数を使ったコードのテスト中に、特定のアサーションでエラーがある閾値を超えないことを確かめていました。例えば、その閾値が0.5だったとしましょう(正確な数値は重要ではありませんが、IRCのログで確認できます)。
そのとき、Hypothesisは「発見しました!ここにエラーが0.500001で起こるケースがありますよ。これはバグですね」と指摘しました。
ネッドは「また浮動小数点の精度問題か」と思いましたが、詳細な調査により、実際には問題ではないことが分かりました。理論上エラーはどんなに大きくなる可能性もありますが、Hypothesisは可能な限り小さいエラー値で発生するケースを見つけ出しました。
これは、実はバグではなく、HypothesisやQuickCheck、その他の同様のツールの設計の一環でした。
テストケースの縮小は、バグを示す最も単純な例を作り出すように設計されています。例えば、特定のスコアが閾値を超えるとバグが生じる場合、そのスコアが例のサイズに応じて増加する傾向にあると、プロパティベースのテストライブラリが提示する失敗例は、そのスコアが閾値をわずかに超えたものになりがちです。これにより、問題が実際よりも小さく見えがちです。
しかし、これはHypothesisやQuickCheck、その他のプロパティベースのテストツールのバグではなく、ツールが意図した通りに機能していることを示しています。
議論の余地はあるかもしれませんが、これは必ずしも問題ではないかもしれません。Hypothesisはバグを明確に示しており、そのためには簡潔な例を使用することが有効です。これによって、問題の理解が容易になります。
しかし、私たちはより良い方法が存在すると考えています。正確な問題を指摘しているとしても、誤解を招く可能性のある例を示すことは、結果的にユーザーの時間を浪費することにつながるかもしれません。
実際に、私は最近この問題に再び直面しました。テストの不安定さが、問題をより大きく感じさせる原因となっていました。
最近、Smarketsの資金提供を受けて、Hypothesisのパフォーマンスと可読性向上のための作業の一環として、期限機能が導入されました。この機能により、遅いテストケースは失敗と見なされます。つまり、テストが合格するものの期限を超えて実行された場合、「DeadlineExceeded
エラーが発生します。これは通常のエラーと同様に扱われ、Hypothesisは他のエラーと同じように縮小処理を行います(マルチ縮小プロセスを含む)。
この問題は、先に述べた閾値問題と同じです。ここでは、スコア(実行時間)と閾値(期限)があり、スコアが閾値を超えるとテストは失敗します。大きなテストケースは遅くなりがちで、そのため遅すぎるという限界にあるケースが一貫して生じる傾向があります。
問題がないように見えますが、Hypothesisはテストエラーを示すために再現性に依存しています。最小化されたケースが見つかると、そのケースを表示し、例外を出力するためにテストを再実行します。しかし、テストの実行時間には実際に再現性がない場合があります。例えば、最初に201ミリ秒かかったテストが次に199ミリ秒で完了することもあります。これにより、Hypothesisはテストが不安定だと判断し始めます。なぜなら、以前は「DeadlineExceeded
」エラーが発生していたのに、今は発生しないからです。これはIssue 892に繋がり、ここではFlorian BruhinがQutebrowserのテストでまさにこの問題に直面しました。
解決策として、私が最終的に選んだ方法は、こちらのリンクに示されているように、縮小プロセス中に一時的に期限を実際の期限と私たちが観測した最大の実行時間の中間値に設定することです。これにより、期限を超えるより大きな閾値に縮小することが保証され、その後の再実行時には実際の期限を快適に超えることが可能です。ただし、テストのパフォーマンスが非常に不安定な場合は除きます(その場合はエラーメッセージも改善しています)。
この解決策は現在、期限の問題に特化していますが、それは問題ではありません。完全に一般的な解決策に急ぐ必要はなく、特に期限はタイミングの信頼性が低いため、他のバリアントとは異なる制約があります。しかし、将来的にはこれをより一般的な解決策に発展させることを考えています。
私が以前から考えていたアイデアの一つに、Hypothesisに何らかのスコアリング概念を追加する、というものがあります。例えば、ユーザーがテストの進行状況を記録するためのスコアを設定できるようにすることです(テストゲームでは、スコアは達成したレベルや文字通りのゲーム内スコアであると想定されます)。これは、そのような用途にもう一つの良い例となるでしょう。もしHypothesisにスコアを利用可能にする方法があれば(あるいはHypothesisが自動的にそれを認識できれば)、上記の解決策を試みることができるかもしれません。例えば、Hypothesisが縮小された例のスコアが元の例のスコアと大幅に異なることに気付いた場合、元の例のスコアに近いようにスコアを維持するための追加的な制約を持って縮小プロセスを再実行することです。そして、大きな(または小さな)スコアを持つ新たに縮小された例を提示し、新しい複数の失敗報告の一部として機能し、両方の例を比較して表示します。
このアイデアを実装する前に、さらに検討する必要があることは確かですが、Hypothesisの使いやすさを向上させ、プロパティベースのテストの効果を高めるためには、この問題の解決が重要です。縮小は、テストがユーザーに示す問題を理解しやすくする素晴らしい第一歩ですが、それはまだ始まりに過ぎません。開発者が示された問題をデバッグする際の生産性を向上させるためには、さらなる努力が必要です。