Skip to content

Instantly share code, notes, and snippets.

@Gab-km Gab-km/papylon_doc.ja.rst
Last active Dec 8, 2015

Embed
What would you like to do?
ぱぴろん非公式ドキュメント

ぱぴろん非公式ドキュメント

ぱぴろんとは?

ぱぴろん(Papylon)は、満たすべき性質(property)を記述することでテストケースを自動生成する Python 用テスティングツールです。いわゆる"QuickCheck"系の流れを汲み、 FsCheckScalaCheck の影響を強く受けています。テストしたい対象の振る舞うべき性質を記述して実行すると、テストケースをランダムに生成して実行し、性質が成り立つかどうかを確認します。

簡単な例

ぱぴろんでテストを記述するためには、だいたい次のモジュールを必要とします:

  • 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())

引数候補のオブジェクトについては後述します。述語関数は、真理値を表現する値を返す関数で、 TrueFalse はもちろんのこと、Python で真理値として一般的に使用されるその他の値(None0[] など)を返しても構いません。

引数候補

引数候補(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):
> [[]]

(記述中)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.