Property-based Testingの使い方

Property-based Testingとは、ランダムに生成された多様な入力値に対し、テスト対象が一定の特性を常に満たすかをテストする手法です。ごく簡単な例としては次のようなテストです。

テスト対象入力された整数値に0を掛ける関数
満たして欲しい特性テスト対象の出力値が常に0
テスト内容ランダムな整数値を関数に入力し、出力を0と比べる、という処理を繰り返す。
Property-based Testingの例

一般的なテスト(Example-based Testing)では一つ一つのテストケースを手動実装するのに対し、Property-based Testingでは入力の範囲とテスト対象が満たすべき特性を包括的に記述します。その範囲でテスト対象への入力値をランダムに自動生成し、常に満たすべき特性を確認する、という処理を何度も繰り返すことで、より効率的かつ強力にバグを検出できる場合があります。また、テストによって仕様を記述するビヘイビア駆動開発の観点でも、個別のテストケースを書き並べるよりも見通しが良くなる場合があります。

この記事では、Property-based Testingを実践する場合に、どのようにテストを記述すれば良いのか、勘所を紹介します。

また、PythonでProperty-based Testingを実行するためのHypothesisについて、以下の記事で説明してします。ご興味のある方は合わせてご覧ください。

目次

出力が特定の値になるかテストするのは難しい

まずは、Property-based Testingでどのようにテストを実装すれば良いのか説明する前に、他のテスト手法も合わせて一般的に、テストの通過条件を決定することの難しさについて考えてみましょう。

例としてExample-based Testingのいくつかのテストケースを想像してみます。

  • あなたはカーナビの最短経路計算ロジックを実装しています。ある道路地図と現在地・目的地を入力したら、10kmのある経路が出力されました。さて、この結果は正しいでしょうか?もっと短い9kmや8kmの距離の経路が存在しないとどうして言えるでしょうか?
  • 独自文法のパラメータ設定テキストをデシリアライズする関数を実装しています。ある設定テキストを入力したところ、あるパラメータ群が得られました。この結果は元のテキストと正しく整合しているでしょうか?
  • 302169+1958776*21を計算したところ、41436465という結果が得られました。この結果は正しいでしょうか?

これらの例から分かるように、テスト対象が本来どういう出力を計算すれば望ましいのかは簡単なロジックでは判断できません。人間は簡単に判断できてもプログラム化が難しい場合や、人間でも正しいか判断が難しいものなど様々です。

もし入力に対して望ましい出力結果を自動計算するテスト用プログラムがあったとしたら、それが元々テスト対象として作りたかったものだというタマゴ・ニワトリ問題になります。このような、テスト対象の望ましい出力を求めることは元のテスト対象を正しく実装するのと同じくらい難しいという問題は、Test Oracle Problem(テストオラクル問題)と呼ばれています。

Test Oracle Problemを回避してどのようにテストケースを作れば良いのかが、Property-based Testingに限らず一般的なソフトウェアテスティングの課題となります。

出力そのものではなく満たすべき特性をテストする

Test Oracle Problemが課題になる理由の一つは、テスト対象の出力が望ましい値と完全一致することをテスト通過条件にしようとしているためです。出力そのものではなく、テスト対象(の出力)が満たすべき特性(Property)のテストであれば、比較的簡単かつ短いコードで実装することができる場合も多いです。

なのでProperty-based Testingなのです。Example-based Testingでも出力そのものではなく満たすべき特性をテストすることもありますし、Property-based Testingの説明として入力を多数生成することばかりに着目してしまうこともありますが、実はちゃんと名は体を表していました。

満たすべき特性の内容は当然テスト対象に依存しますが、大まかなやり方は先人によって整理されています。この節ではいくつかの方法を紹介します。

関数と逆関数のテスト

関数y=f(x)に対してx=g(y)を計算する関数を逆関数と言います。

このように説明されると純粋数学にしか関係のない概念に思えてしまいますが、実はソフトウェア開発でも非常に頻繁に現れる概念です。

関数と逆関数のパターン
二回実行すると元に戻る処理配列の反転
対になっている処理のペアシリアライズとデシリアライズ
暗号化と復号化

逆関数を持つ関数のテストや、関数と逆関数のセットのテストについては、任意の入力について関数の出力結果に逆関数を適用したら正しく元に戻るか、をテストする方法が考えられます。

Python Hypothesis風に書くと以下のようになります。

from hypothesis import given, strategies

@given(strategies.text())
def test_g_inverts_f(s):
    assert g(f(s)) == s  # fが関数、gが逆関数

なお、点の座標回転のように、2回ではなく一定回適用したら元に戻る処理でも同じ方法でテスト可能です。

Metamorphic Testing(メタモルフィックテスティング)

Metamorphic Testingとは、ある入力xに対するテスト対象の出力yと、xにある変換を施したx'に対する出力結果y'を比較する手法です。

説明によく使われる例は正弦関数sinです。任意の実数xについて、x'=x+piと置くとy=sin(x)y'=-sin(x')は常に等しくなります。

こちらについても、数学的な関数だけでなくソフトウェア開発でも以下のようなテストが考えられます。

テスト対象Metamorphic Testing
集合、優先度付きキューなどのデータ構造どのような順序で値を入力しても最終的なデータ構造が等しくなるか
何らかの推薦システムにおける順序決定推薦対象群Xの順序が、Xの部分集合の推薦でも維持されているか
仕様は変わらないリファクタリングリファクタリング前後の関数の結果が一致するか

異常停止や例外発生のテスト

そもそも、出力値が満たすべき特性を個別に考えずとも、計算の途中で不正に止まらずに値が常に出力されることもテスト対象が満たすべき特性です。

Property-based Testingのライブラリは、単体モジュールレベルのモンキーテスト用の便利なフレームワークとしても利用することができます。例えばPythonのHypothesisでは、出力に対するアサーション以外でも、テストの実行中にキャッチした例外は全てテスト失敗として報告してくれます。

参考文献

この記事のいくつかの用語は、細かいニュアンスまで含めて厳密な定義がある言葉ではまだないと思いますし、文献によっても微妙に定義が異なりますが……、それでも上記の説明は明らかに違うよという方がいたらごめんなさい。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

ITベンチャーでデータ分析、AI開発、システム設計、提案、営業、組織管理、公演、採用などなど多数の役割に従事してきました。

様々な職業や背景の方々と交流するうちに、幅広い分野で問題を解決したり価値を生み出したりするためには、個別の知識だけでなく、汎用的に物事を考える力を伸ばしていく必要があると考えるようになりました。

更に、自分自身の考える力だけでなく、より多くの人々の考える力のトレーニングを応援することで、社会全体を良くしていけるのではないかと考えて、このサイトを作りました。

目次
閉じる