プロパティベーステストの概要とPythonでの実装例

 「実践プロパティベーステスト」の発売をきっかけに、国内でプロパティベーステストの話題がホットになっています。
 今回はこのプロパティベーステストの概要とテクニックについて、Pythonをサンプルに解説します。

プロパティベーステストとは

 プロパティベーステストは、「プロパティ」が成立するかを確認するテストです。ここでいうプロパティは「事前条件に対する事後条件・不変条件の関係を、実行可能にしたもの」です。

 このプロパティ、プロパティベーステストは、よく実例ベーステスト(Exampleベーステスト、事例ベーステスト)と対比して解説されることが多いため、今回もそれに習います。

 まず実例ベーステストは、テスト対象の入力と出力の実例を、テスト条件と期待値に実装するアプローチです。
 例えばソート関数を対象とする場合を考えます。実例ベーステストの場合、「リスト[2, 1, 3]を渡すと、結果としてリスト[1, 2, 3]が出力される」のように、具体的な実例値[2, 1, 3]、[1, 2, 3]を使ってテスト実装します。
 一般的なソフトウェアテストの多くは、この実例ベーステストに該当します。そこではテスト実装で入力・期待値に具体的な実例値を指定し、それに従ってテスト実行するアプローチをとります。

 一方プロパティベーステストは、入力・期待値の実装に実例を使用しません。入力を自動生成しながら「事前条件に対する事後条件・不変条件の関係」が成立するかを確認します。
 例えばソート関数を対象とするならば、「リストを入力すると、結果として昇順にソートされたリストが出力される」ことを、様々な入力を生成しながら確認します。
 なお、ここでの「結果として昇順にソートされたリストが出力される」の確認のテスト実装が難しい場合があります。その場合、「結果として先頭に入力の最小値が配置されたリストが出力される」と緩和化するような、様々な工夫を使う必要があります。この工夫は後述します。

プロパティベーステストの利点・欠点と立ち位置

 プロパティベーステストの利点に以下があります:

  • テスト詳細設計、テスト実装を自動化できる
  • 上記のメリットにより、テスト条件、テストケースを自動生成しやすい。それによりテスト条件の詳細な網羅が可能になる。
  • 上記のメリットにより、実例によるサンプリングチェックではなく、事前条件・事後条件・不変条件の仕様の実現を確認できる。

 プロパティベーステストは一方で次のような欠点があります:

  • テストオラクル問題(テストの期待値をどう確保・実装するかの問題)を抱える。それにより実例ベーステストのような詳細な期待値比較ができない場合がある。
    • 例えば「リストを入力すると、結果として昇順にソートされたリストが出力される」のプロパティを確認する場合では「結果として昇順にソートされたリストが出力される」の確認の実装が必要になる。しかしそれはプロダクトコードと重複する二重実装になってしまい、価値の低い確認になってしまうことが少なくない。その際は、プロパティの簡略化や、知恵を使った代替手段に頼る形になる。
  • 自動化前提のアプローチである。そのため自動化困難なテストでは効果を出しにくい。
  • 具体例を明示しないため、UXやユーザーストーリーといった、実物を実際に触って得られる直感的で具体的な実例が必要なテストに適用しにくい。

 このように利点・欠点双方あることから、プロパティベーステストは実例ベーステストを包括する・優れる、という関係ではありません。お互いの利点を活かしあう補完関係を取ります。

プロパティベースの具体例

 ソートメソッドsortedをテスト対象に、プロパティベーステストを実装した例を示します。このテストは、整数リストを入力で渡したら、昇順にソートされたリストが出力されることを確認するものです。

 まず実例ベーステストでの実装例を示します:

# 実例ベーステスト
def test_sort_example_based():
    assert sorted([2, 1, 3]) == [1, 2, 3]

 「[2, 1, 3]」という実例の入力をテスト対象に与えて、「[1, 2, 3]」という実例の期待値が出力と一致することを確認しています。

 次に、プロパティベーステストの実装例を示します。Pythonではファジングでよく使われるhypothesisで実装します。

# プロパティベーステスト
@given(st.lists(st.integers(), min_size=1))
def test_sort_property_based(input_list):
    result = sorted(input_list)
    assert len(result) > 0
    assert min(result) == result[0]
    assert max(result) == result[-1]

 「昇順にソートされたリストが出力される」を素直に確認できればベストなのですが、それを実装するとなるとsortedの二重実装を行うことになるため、事後条件を次のような確認で代替しています。

  • リストが空で出ないこと
  • リスト先頭が、入力の最小値であること
  • リスト末尾が、入力の最大値であること

 この代替により、実例ベーステストと比べ、プロパティベーステストは限定的なテストしかできないようになっています(先頭・末尾しか見ておらず、それ以外の要素がソートされているかは確認できていない)。その犠牲でプロパティベーステストの利点を得られるようにしています。

 プロパティベーステストでは、入力値は自動生成します。上記サンプルの場合「@given(st.lists(st.integers(), min_size=1))」で指定された「要素1個以上の整数値のリスト」をランダムに大量生成して、プロパティが成立するか確認します。

プロパティベーステストでのテストオラクル問題への対応アプローチ

 前述の通り、プロパティベーステストではテストオラクル問題が主要な課題になります。この課題への対策として色々なテクニックが考案されています。その一部を以下にまとめます。

対称性のある関係を活用する:ラウンドトリップテスト

 「エンコードしてデコードする」「シリアライズしてデシリアライズする」「保存して読みだす」のように、「処理」と「逆処理」という対象性のある機能を使います。
 その機能を使って、入力データに処理をかけ、その結果に逆処理をかけた結果が、元の入力データと一致するか確認するテストを実装します。
 このテクニックの詳細は以下を参照ください

テストオラクルに依存しないテスト実装テクニック:ラウンドトリップテスト(Roundtrip Test) - 千里霧中

推定可能な入出力の組の関係性を活用する:メタモルフィックテスト

 入出力が推定可能な関係を活用します。それにより、例えば異なる入力でも出力が同じになるようなパターンを使って、異なる入力ごとに実行した結果が一致することをもって、プロパティの成立を確認します。
 一例としてsin関数を対象とした場合を考えます。sinの入出力の関係として「sin (π − input)」と「sin (input)」は常に数学的に一致します。そこで、その関係性を利用し、同じ入力で「sin (π − input)」と「sin (input)」を実行し、それぞれの結果が一致することを確認するテストを実装します。

仕様モデルから期待値を得る:モデル駆動アプローチ

 実行可能なモデルでモデルベーステストを行う場合、モデルをテストオラクルにできる場合があります。
 その場合、プロパティベーステストで自動生成した入力値を、実行可能なモデルと、テスト対象に入力し、それぞれの結果が一致するかを確認するテストを実装できます。

プロパティベーステストと類似概念

メタモルフィックテスト

 AIシステムのテストのテクニックであるメタモルフィックテスト(メタモルフィックテスティング)は、プロパティベーステストの一種です。
 ただし、プロパティベーステストの名づけのタイミングが新しめのため、従来からメタモルフィックテストを使っている人達にとっては、プロパティベースドテストに内包されるという認識がない場合があります。
 なお、メタモルフィックテストで研究・蓄積されているテクニックは、プロパティベーステストのテストオラクル問題の改善に有益で勉強の価値があります。

QuickCheck

 HaskellのQuickCheck(QuickCheckテスト)は、プロパティベーステストと同義です。QuickCheckを広く利用できるように汎用化・普遍化したものがプロパティベーステストと言ってよいと思います。
 ただしQuickCheckの方がより古くから活用され、プロパティベーステストはそれから派生したと解釈できる余地もあるため、プロパティベーステストの呼称を避ける場合があります。

ファジング

 ファジングとプロパティベーステストの関係は、現状としてコミュニティやコンテキストに依存しています。
 伝統的なファジングは、プロパティベーステストのような詳細なプロパティの確認を行いません(システムに異常がないか、エラーが記録されていないかといった、かなりざっくりとした事後条件を確認する)。そのファジングは、プロパティベーステストとは概念が少しずれています。またプロパティベーステストはQuickCheckのようにランダムテストを許容しますが、伝統的なファジングはバグがでそうな高リスクな入力パターンを使うという違いもあります。
 一方、Go Fuzzのような一部のコミュニティや技術が呼称するファジングは、プロパティベーステストと同義です。