プロパティベースドテスティングとは何か
以下の文章はこちらの記事を翻訳したものです。
よく「プロパティベースドテスティングとは何ですか?」と尋ねられます。私の専門はプロパティベースドテスティングのツール開発ですが、実はこれまでこの質問に対する明確な答えを持っていませんでした。この記事は、その問題に取り組む試みです。
以前まで、プロパティベースドテスティングの定義は「QuickCheckが行うこと」でした。これは実用的な定義としてはうまく機能していますが、問題点として、プロパティベースドテスティングの本質的な特徴と、私たちが普段使う実装の偶発的な特徴を区別することが難しくなります(詳細はこちら)。
QuickCheckとは大きく異なるプロパティベースドテスティングシステムの開発者として、この問題は私にとって特に重要です。そこで、プロパティベースドテスティングが何であるか、また何ではないかについて、私自信の見解を述べてみたいと思います。
これは最終的な定義ではなく、私の考え方が進化するにつれて変わるものですが、今後の討論のための良い出発点になるでしょう。
このような概念の境界線を引く方法は基本的に二つあります。一つは狭い範囲で定義し、QuickCheckの定義のように限定する方法です。もう一つは、より広い範囲で定義し、同じ一般的な行動パターンを持つものすべてを含める方法です。私は通常、より広い範囲の定義を好みますが、この記事では敢えて自分の好みは抑え、狭い範囲の定義に焦点を当てようと思います。
しかし、始める前に一つ明確にしておきたいことがあります。以下に挙げるものは、プロパティベースドテスティングの本質的な特徴ではありません:
- 参照透過性
- 型システム
- ランダム化
- 特定のツールやライブラリの使用
これを証明するために、以下の事例を提示します:
- HypothesisやQuickCheck(Erlang版およびHaskell版を含む)など、ほとんどのプロパティベーステスティングライブラリ
- ErlangのQuickCheck、test.check、Hypothesisなど、動的言語向けの成功した多くのプロパティベーステスティングシステム
- SmallCheck。これに関しては複雑な感情もありますが、明らかにプロパティベースのテスティングシステムです。
- 特定の結果のために自分で作るプロパティベーステスティングプロトコル。例として、以前私はコードフォーマッターのテストのためにPythonファイルのコーパスを用い、結果として得られるフォーマットされたコードがPEP8準拠であるかをチェックしました。これはオラクルを用いた典型的なプロパティベースドテスティングの一例です。
これにより、プロパティベースドテスティングについての有益な出発点が得られます。しかし、肯定的な例だけで良い定義を見つけることはできません。より議論の余地があるケースに目を向けてみましょう。
まず、括弧内の質問に戻ります:大規模なコーパスにのみ基づいてテストを行うことはプロパティベースドテスティングと見なされるのでしょうか?
私の考えでは「おそらくそう」です。SmallCheckを含めるなら、大規模なコーパスに基づくテストも含めるべきだと思います。例えば、SmallCheckが生成する最初の20,000の結果を取り、それらのうち最初のN個だけを毎回使ってテストを行う場合、それはまさに同じ種類のテストです。同様に、Hypothesisを使用して20,000の結果を生成し、それらの中から毎回ランダムに選択してテストする場合も同じです。
小さく固定されたコーパスから選ぶ場合は、おそらくプロパティベースドテスティングとは見なされないでしょう。たとえば、ソースコード内に10個の例に基づくテストをプロパティベースドテストとして実際に書くことができれば、それは実際には例に基づくテストに過ぎない可能性があります。この境界は少し曖昧ですが。
では、ファジングについてはどうでしょうか?
以前はファジングもプロパティベースドテスティングの一形態だと考えていました。というのも、ファジングは「クラッシュしない」というプロパティをテストするものだからです。しかし、最近になって私の意見は変わりました。特に、Hypothesisを使ったテストスタイルを推奨している私の記事で紹介されている方法は、おそらくプロパティベースドテスティングとしてはカウントされないでしょう。
この境界に関してはまだ確信が持てていません。主な理由は、ファジングとプロパティベースドテスティングが異なる特性を持っているように感じられるからです。プロパティベースドテスティングでは、プログラムがどのように振る舞うべきかについての推論が必要ですが、ファジングはプログラムの振る舞いを深く理解することなく任意のプログラムに適用できます。そして、ファジングはなんとなくより基本的なアプローチのように思えます。
しかし、ファジングツールを用いてプロパティベースドテスティングを行うことは十分に可能ですし、手作りのプロパティベースドテスティングシステムで行うのと同じ方法で実施することができます。例えば、上述のPythonフォーマットテストにpython-aflを追加しても、それは依然としてプロパティベースドテスティングでしょう。
逆に、プロパティベースドテスティングツールを使ってファジングを行うことも可能です。もしファジングがプロパティベースドテスティングではないとするならば、HypothesisやQuickCheckを使用したすべてのテストがプロパティベースのテストであるとは限りません。実際には、私はそれで問題ないと考えています。テストツールがその元々の領域外で使用されることは長い伝統があります。例えば、もともと単体テストツールとして設計されたほとんどのテストフレームワークが、テストの全範囲にわたって使用されるようになるのは一般的なことです。
それを踏まえて、ファジングに対する私の定義を提供しましょう:
- ファジングとは、大規模なコーパスからのデータをコード(関数、プログラムなど)に供給し、そのコードが失敗するかどうかを確認するプロセスです。このデータは動的に生成されたり、以前のデータの実行結果に基づいて生成されたりします。
「データ」と「失敗するかどうか」の定義はファジャーによって異なります。一部のファジャーはバイナリデータのみを生成するかもしれませんし、より構造化されたデータを生成するものもあります。また、一部のファジャーはプロセスのクラッシュを探しますが、単に関数がfalseを返すことを探すものもあります。
(ファジングの定義がしばしば「不正な」データに焦点を当てることについては、私はこれが誤解を招くと考えます。このような定義は、明らかにファジャーとみなされる多くのツールを考慮に入れていません。例えば、CSmithは確かにファジャーの一種ですが、意図的に正しい形式のCプログラムのみを生成します)。
上述のファジングの定義を踏まえることで、私自身のプロパティベーステスティングの定義を提唱することができます:
- プロパティベースドテスティングとは、テストがファジングされた際に、そのテストの失敗がシステムの問題を明らかにするようなテストの構築です。特に、これらの問題はシステムの直接的なファジングでは明らかにならないものです。
(もしファジングがプロパティベースドテスティングとみなされるべきだと強く感じる場合は、「明らかにならない」という部分を単に省略してください。私自身はこの点に関しては中立的です。)
これらの追加的な失敗モードは、私たちがテストしているプロパティを構成するものです。
私はこれがプロパティベースドテスティングにおける私たちの活動をよく捉えていると思います。完璧ではないかもしれませんが、私はこれにかなり満足しています。特に気に入っている点は、プロパティベースドテスティングがコンピュータが行う作業ではなく、人が行うプロセスであることが明確にされていることです。コンピュータの役割は「単なるファジング」に過ぎません。
この観点から見ると、プロパティベースドテスティングライブラリは実際には二つの部分から成り立っています:
- ファジャー。
- そのファジャーを利用してプロパティベースのテストを構築することを容易にするためのツールのライブラリ。
Hypothesisは、このラインに沿って非常に明確に設計されています。その核心部分はConjectureという名の構造化されたファジングライブラリです。これは少し私の偏見があるかもしれませんが、それでも、他のほとんどのプロパティベーステスティングシステムの振る舞いをかなりよく捉えていると思います。また、私が望んでいたよりも広い定義と、QuickCheckが提供するよりも厳密に焦点を当てた定義との間の良い中間地点を提供できていると言えるでしょう。