ぱぴろん(Papylon)は、満たすべき性質(property)を記述することでテストケースを自動生成する Python 用テスティングツールです。いわゆる"QuickCheck"系の流れを汲み、 FsCheck と ScalaCheck の影響を強く受けています。テストしたい対象の振る舞うべき性質を記述して実行すると、テストケースをランダムに生成して実行し、性質が成り立つかどうかを確認します。
ぱぴろんでテストを記述するためには、だいたい次のモジュールを必要とします:
papylon.prop
papylon.arbitrary
papylon.checker
ここで、リストを反転してさらに反転させたものは元のリストに戻る、という性質を確認してみましょう:
from papylon.prop import for_all
from papylon.arbitrary import arb_list, arb_int
from papylon.checker import check
# reversed and reversed list is the same of the original list
p1 = for_all([arb_list(arb_int(), max_length=20)],
lambda x: list(reversed(list(reversed(x)))) == x)
check(p1)
このコードを実行すると、次のように表示されます:
OK, passed 100 tests.
確認しようとした性質が成り立たない場合、ぱぴろんは反例を表示します:
import math
p2 = for_all([arb_int()], lambda n: math.sqrt(n*n) == n)
check(p2)
Falsified after 2 tests (31 shrinks):
> [-1]
性質(property)は、ぱぴろんにおけるテスト単位で、 papylon.prop.Prop
クラスのインスタンスとして表現されます。性質を記述するための関数はいくつか用意されており、その代表的なものが、上記の例でも使用した papylon.prop.for_all
関数です。 for_all
関数は、引数候補(arbitrary)のリストと、満たすべき性質を記述した述語(predicate)関数を必要とします:
from papylon.arbitrary import arb_date
import datetime
p3 = for_all([arb_date()],
lambda dt: (dt + datetime.timedelta(days=7)).weekday() == dt.weekday())
引数候補のオブジェクトについては後述します。述語関数は、真理値を表現する値を返す関数で、 True
や False
はもちろんのこと、Python で真理値として一般的に使用されるその他の値(None
や 0
、 []
など)を返しても構いません。
引数候補(arbitrary)は、性質が成り立つかどうか確認する際に述語関数に渡される引数を生成するオブジェクトです。生成したい値の型ごとにデフォルトのクラスがあり、それぞれ整数型、浮動小数点数型、日付型、文字型、リスト型、文字列型が papylon.arbitrary
モジュールに定義されています。Python は動的に型付けするタイプの言語なので、厳密に型に従う必要はありません。ですが、値の生成パターンに制限をかけた方が性質を正しく確認しやすいです。特に新しく引数候補を定義する際には意識しておくといいでしょう。
引数候補オブジェクトは、値を生成する arbitrary
メソッドと、より小さな反例を見つけ出す操作(これを「シュリンク」と言います)を行う shrink
メソッドを持ちます。デフォルトの引数候補はこれらのメソッドを持った papylon.arbitrary.AbstractArbitrary
クラスを継承していますが、自分用の引数候補クラスを作る際に、必ずしもこのクラスを継承する必要はありません。
デフォルトの引数候補はフィールドに papylon.gen.Gen
クラスのインスタンス(「ジェネレータ」と呼びます)を持っています。ジェネレータの generate
メソッドの戻り値を、 arbitrary
メソッドの戻り値として定義しています。ジェネレータは値のランダム生成に使用するオブジェクトで、これを生成するために papylon.gen
モジュールの関数群が使用されています。 Gen
クラスは内部的に Python のジェネレータ型オブジェクトを持っており、ジェネレータをうまく定義することで、それなりのランダム性で値を生成し続けることができます。
デフォルトの引数候補はさらに、 papylon.shrinker.AbstractShrinker
クラスのサブクラス(「シュリンカ」と呼びます)をフィールドに持っています。シュリンカの shrink
メソッドは反例となった引数リストを受け取り、さらに小さな反例を見つけるためのイテレータオブジェクトを返します。デフォルトの引数候補が持つ shrink
メソッドは、この戻り値を返しています。シュリンクという操作は反例があっても必ず実行されるわけではなく、 papylon.prop.for_all_shrink
関数を使って生成された性質で実行されます。(for_all
は実際には for_all_shrink
の結果を返しています)
ある範囲の値を生成するジェネレータは choose
関数で定義することができます。例えば、1から100の値をランダムに生成するジェネレータは次のように作ることができます:
from papylon.gen import choose
gen = choose(1, 100)
print(gen.generate()) # print a number from 1 to 100
one_of
はジェネレータのシーケンスから1つジェネレータを生成する関数です:
from papylon.gen import one_of, constant
gen = one_of(list(map(constant, ['ham', 'egg', 'spam'])))
print(gen.generate()) # print any string of 'ham', 'egg' or 'spam'
frequency
もいくつかのジェネレータから1つジェネレータを生成する関数ですが、 one_of
との違いは候補の選び方に重み付けができる点です:
from papylon.gen import frequency, constant
gen = frequency([(9, constant(False)), (1, constant(True))])
print(gen.generate()) # print True or False, you will see False about 90% of the time
such_that
メソッドは、ジェネレータに条件をつけて、生成する値を絞り込むことができます:
gen1 = choose(1, 100)
gen2 = gen1.such_that(lambda i: i <= 50)
print(gen2.generate()) # print a number from 1 to 50
map
メソッドは、ジェネレータが生成する値を変換した新しいジェネレータを作ります:
gen1 = choose(1, 10)
gen2 = gen1.map(lambda n: n ** 2)
print(gen2.generate()) # print any number of 1, 4, 9, ..., 81 or 100
papylon.checker
モジュールには check_and_assert
という関数があります。これは同じモジュールの check
関数のように性質を実行しますが、結果を標準出力に出すのではなく、失敗時のメッセージを AssertionError
に包んで投げるようになっています。これを使うことで、既存のテストツールにぱぴろんを実行させることができます。
# test_sample.py
from papylon.prop import for_all
from papylon.arbitrary import arb_int
from papylon.checker import check_and_assert
def test_commutative():
p1 = for_all([arb_int(), arb_int()], lambda x, y: x + y == y + x)
check_and_assert(p1)
def test_associative():
p1 = for_all([arb_int(), arb_int()], lambda x, y: 2 * (x + y) == 2 * x + y) # wrong property!!
check_and_assert(p1)
このテストを py.test に食べさせてみましょう。
(.venv) $ py.test test_sample.py
============================= test session starts =============================
platform win32 -- Python 3.5.0, pytest-2.8.2, py-1.4.30, pluggy-0.3.1
rootdir: C:\User\Gab-km\src\py\papylon_doc\tests, inifile:
collected 2 items
test_sample.py .F
================================== FAILURES ===================================
______________________________ test_associative _______________________________
def test_associative():
p1 = for_all([arb_int(), arb_int()], lambda x, y: 2 * (x + y) == 2 * x + y) # wrong property!!
> check_and_assert(p1)
(中略)
def assert_result(result):
text, is_proved, _ = convert_to_outputs(result)
if not is_proved:
> raise AssertionError(text)
E AssertionError: Falsified after 1 test (30 shrinks):
E > [2, 1]
.venv\lib\site-packages\papylon\utils.py:58: AssertionError
===================== 1 failed, 1 passed in 0.06 seconds ======================
このように、失敗した際の入力が通知されます。
ぱぴろんでは、 papylon.prop.Properties
クラスと papylon.checker.check_all
関数により、複数の性質をまとめて確認することも可能です。
from papylon.prop import for_all, Properties
from papylon.arbitrary import arb_int
from papylon.checker import check_all
props = Properties("List propositions")
props.add("length proposition", p1)
props.add("wrong proposition",
for_all([arb_list(arb_int(), max_length=20)], lambda xs: len(xs) < 0))
check_all(props)
これを実行すると、 グループ名.性質名 -> 結果 という形式で確認結果が出力されます。
List propositions.length proposition -> OK, passed 100 tests.
List propositions.wrong proposition -> Falsified after 1 tests (9 shrinks):
> [[]]
(記述中)