詳細と高度な機能
この記事は、公式ガイドの「 仮説-詳細および高度な機能 」ページの翻訳です 。
2017年11月23日にアレクサンダー・ショーリンが「モスクワパイソンミートアップ50」で発表したことを除いて、ロシア語で仮説の使用に関する有用な情報を見つけることができませんでした。 私はそれを理解することにしました。 その結果、私は何かを翻訳しました。
このパートでは、仮説のあまり一般的ではない機能を検討します。これは、使用を開始するために必要ではありませんが、それでも生活を楽にします。
オプションのテスト出力
通常、失敗したテストの結果は次のようになります。
Falsifying example: test_a_thing(x=1, y="foo")
各名前付き引数のrepr
とともに出力されます。
repr
には値があり、あまり説明的ではないため、またはテストのいくつかの中間ステップの結果を確認する必要があるため、これでは十分でない場合があります。 note
機能は次のとおりです。
>>> from hypothesis import given, note, strategies as st >>> @given(st.lists(st.integers()), st.randoms()) ... def test_shuffle_is_noop(ls, r): ... ls2 = list(ls) ... r.shuffle(ls2) ... note("Shuffle: %r" % (ls2)) ... assert ls == ls2 ... >>> try: ... test_shuffle_is_noop() ... except AssertionError: ... print('ls != ls2') Falsifying example: test_shuffle_is_noop(ls=[0, 1], r=RandomWithSeed(18)) Shuffle: [1, 0] ls != ls2
テストの最後の実行では、テストで必要になる可能性のある追加情報を含むメモが印刷されます。
テスト統計
pytest
を使用すると、実行されたテストに関する一連の統計を見ることができます
コマンドライン引数--hypothesis-show-statistics
を渡し--hypothesis-show-statistics
。 可能にするもの
いくつかの一般的なテスト統計:
たとえば、 --hypothesis-show-statistics
次を実行した場合:
from hypothesis import given, strategies as st @given(st.integers()) def test_integers(i): pass
表示されます:
test_integers: - 100 passing examples, 0 failing examples, 0 invalid examples - Typical runtimes: ~ 1ms - Fraction of time spent in data generation: ~ 12% - Stopped because settings.max_examples=100
最後の行「Stopped because」は特に重要です。これは、テストが新しいサンプルの試行を停止するタイミングを決定する設定値を示します。 これは、テストの動作を理解するのに役立ちます。 理想的には、常にmax_examples
がmax_examples
です。
場合によっては(たとえば、フィルターや再帰的ストラテジーなど)、データ生成のいくつかの側面を説明するイベントが表示されます。
from hypothesis import given, strategies as st @given(st.integers().filter(lambda x: x % 2 == 0)) def test_even_integers(i): pass
次のような結果になります。
test_even_integers: - 100 passing examples, 0 failing examples, 36 invalid examples - Typical runtimes: 0-1 ms - Fraction of time spent in data generation: ~ 16% - Stopped because settings.max_examples=100 - Events: * 80.88%, Retried draw from integers().filter(lambda x: <unknown>) to satisfy filter * 26.47%, Aborted test because unable to satisfy integers().filter(lambda x: <unknown>)
event
関数を使用して、テストでカスタムイベントをマークすることもできます。
hypothesis.event(値)
from hypothesis import given, event, strategies as st @given(st.integers().filter(lambda x: x % 2 == 0)) def test_even_integers(i): event("i mod 3 = %d" % (i % 3,))
次に、結果が表示されます。
test_even_integers: - 100 passing examples, 0 failing examples, 38 invalid examples - Typical runtimes: 0-1 ms - Fraction of time spent in data generation: ~ 16% - Stopped because settings.max_examples=100 - Events: * 80.43%, Retried draw from integers().filter(lambda x: <unknown>) to satisfy filter * 31.88%, i mod 3 = 0 * 27.54%, Aborted test because unable to satisfy integers().filter(lambda x: <unknown>) * 21.74%, i mod 3 = 1 * 18.84%, i mod 3 = 2
event
引数はどのタイプでもかまいませんが、 str
文字列に変換するときに一致する場合、2つのイベントは同じと見なされstr
。
仮定を使用する
時には仮説はあなたが望む正しい種類のデータを正確に与えない-それは基本的に正しい形式です。 これは特に恐ろしいことではありませんが、いくつかの例は失敗し、それらの世話をしたくありません。 早めにテストを中断することで、これらのケースを単純に無視できますが 、誤って重要なものを見逃し、予想よりはるかに少ない経験をするリスクがあります。 また、悪い例に費やす時間を少なくすることも良いでしょう-テストごとに100個の例を使用すると(デフォルトで)、これらの例のうち70個がニーズを満たさないことが判明すると、多くの時間が浪費されることがわかります。
hypothesis.assume(条件)
たとえば、次のコードがあるとします。
@given(floats()) def test_negation_is_self_inverse(x): assert x == -(-x)
それを実行すると、以下が得られます。
Falsifying example: test_negation_is_self_inverse(x=float('nan')) AssertionError
これは面倒です。 私たちは(おそらく)NaNについて何かを知っていますが、このエピソードでは、NaNについて考えたくなく、何らかの形でこの状況を処理したいと思いますが、仮説がNaNの例を見つけるとすぐに、彼はすべてを捨てて、それについて私たちに急ぐでしょう。 テストは失敗し、統計が台無しになりますが、合格したいです。
この特定の例をブロックしましょう:
from math import isnan @given(floats()) def test_negation_is_self_inverse_for_non_nan(x): assume(not isnan(x)) assert x == -(-x)
このバージョンのコードはすでに問題なく通過しています。
簡単な傍受の疑いを排除するために、意図したものよりもはるかに多くのことを想定できます。仮定に合格する十分な例を見つけることができない場合、仮説はテストに失敗します。
私たちが書いた場合:
@given(floats()) def test_negation_is_self_inverse_for_non_nan(x): assume(False) assert x == -(-x)
次に、起動時に例外が発生します。
Unsatisfiable: Unable to satisfy assumptions of hypothesis test_negation_is_self_inverse_for_non_nan. Only 0 examples considered satisfied assumptions (* (assumptions) hypothesis test_negation_is_self_inverse_for_non_nan. 0 (assumptions)*)
仮定する方法は?
仮説には、仮定を改ざんし、原則として到達困難な状況でも例を見つけることができるという事実につながるケースを回避しようとする適応知能戦略があります。
次のものがあると仮定します。
@given(lists(integers())) def test_sum_is_positive(xs): assert sum(xs) > 0
そのようなテストが失敗し、偽の例を生成することは驚くことではありません[]
。
これにassume(xs)
を追加assume(xs)
、些細な空の例が削除され、 [0]
得られます。
assume(all(x > 0 for x in xs))
追加assume(all(x > 0 for x in xs))
そして、奇跡! 彼は通ります! 確かに、正の合計はゼロよりも大きいです!
驚くべきことは、彼が反例を見つけられないということではなく、十分な例を見つけることです。
興味深いことが起こるようにするには、長いリストでこれを試してください。 たとえば、 assume(len(xs) > 10)
追加assume(len(xs) > 10)
ます。 基本的に、これは例であってはなりません。リストの各要素が半分ネガティブである場合、偶然にそれらのうちの10個を取得する必要があるため、プリミティブ戦略は1000の例のうち1つ未満を検出します。 デフォルトの構成では、仮説は1000個の例を試行するずっと前に降伏します(デフォルトでは200個を試行します)。
これを実行しようとすると、次のようになります。
@given(lists(integers())) def test_sum_is_positive(xs): assume(len(xs) > 10) assume(all(x > 0 for x in xs)) print(xs) assert sum(xs) > 0 In: test_sum_is_positive() [17, 12, 7, 13, 11, 3, 6, 9, 8, 11, 47, 27, 1, 31, 1] [6, 2, 29, 30, 25, 34, 19, 15, 50, 16, 10, 3, 16] [25, 17, 9, 19, 15, 2, 2, 4, 22, 10, 10, 27, 3, 1, 14, 17, 13, 8, 16, 9, 2... [17, 65, 78, 1, 8, 29, 2, 79, 28, 18, 39] [13, 26, 8, 3, 4, 76, 6, 14, 20, 27, 21, 32, 14, 42, 9, 24, 33, 9, 5, 15, ... [2, 1, 2, 2, 3, 10, 12, 11, 21, 11, 1, 16]
ご覧のように、仮説では多くの例は見つかりませんが、成功する結果を得るには十分な例があります。
一般に、テストの戦略をより正確に形成する余裕がある場合は、これを使用する必要があります。たとえば、 integers(1, 1000)
、 assume(1 <= x <= 1000)
よりもはるかに優れintegers(1, 1000)
います。
戦略の定義
テスト関数によって提供される例を調べるために使用されるオブジェクトのタイプは、 hypothesis.SearchStrategy
と呼ばれます。
これらは、 hypothesis.strategiesモジュールで開いている関数を使用して作成されます。
これらの戦略の多くは、世代を調整するために使用できるさまざまな引数を提供します。 たとえば、整数の場合、必要な整数のmax
値とmax
値を指定できます。 戦略の結果を確認したい場合は、例をリクエストできます。
>>> integers(min_value=0, max_value=10).example() 1
多くの戦略は、他の戦略から構築されています。 たとえば、タプルを定義する場合、各要素で何が起こるかを言う必要があります。
>>> from hypothesis.strategies import tuples >>> tuples(integers(), integers()).example() (-24597, 12566)
追加情報:doc: available in a separate document <data>
。
プリセットパラメーターに関する詳細情報
hypothesis.given(* given_arguments、** given_kwargs)
@given
デコレータを使用して、パラメータ化する関数の引数を示すことができます。 位置型引数または名前付き型引数、またはそれらの組み合わせを使用できます。
たとえば、次のすべてが有効です。
@given(integers(), integers()) def a(x, y): pass @given(integers()) def b(x, y): pass @given(y=integers()) def c(x, y): pass @given(x=integers()) def d(x, y): pass @given(x=integers(), y=integers()) def e(x, **kwargs): pass @given(x=integers(), y=integers()) def f(x, *args, **kwargs): pass class SomeTest(TestCase): @given(integers()) def test_a_thing(self, x): pass
次はそうではありません:
@given(integers(), integers(), integers()) def g(x, y): pass @given(integers()) def h(x, *args): pass @given(integers(), x=integers()) def i(x, y): pass @given() def j(x, y): pass
指定されたものの有効な使用方法を決定するためのルールは次のとおりです。
- 指定さ
given
た名前付き引数を渡すことができます。 - 指定された位置引数は、テスト関数の右端の名前付き引数と同等です。
- ベーステスト関数に可変長引数(varargs)、任意のキーワード、またはキーワード専用の引数がある場合、位置引数は使用できません。
-
given
されたものでテストされた機能にはデフォルト値がない場合があります。
「極右の名前付き引数」の動作の理由は、インスタンスメソッドを使用して@given
: self
が通常どおり関数に渡され、パラメーター化されないためです。
givenによって返される関数は、元のテストと同じ引数をすべて持ち、 @given
埋められた@given
。
カスタム機能の実行
仮説は、例を実行する方法を制御できるツールを提供します。
これにより、各サンプルの設定と分解、サブプロセスでのサンプルの実行、コルーチンテストの通常のテストへの変換などのアクションを実行できます。たとえば、 TransactionTestCase
in
Django extraは、個別のデータベーストランザクションで各例を実行します。
したがって、エグゼキューターまたはロシアのアーティストの概念を紹介します。 executorは基本的に、コードのブロックを取得して実行する関数です。 デフォルトのエグゼキューターは次のとおりです。
def default_executor(function): return function()
クラスでexecute_example
メソッドを定義することにより、エグゼキューexecute_example
定義します。 このクラスで@given
デコレータを使用するテストself.execute_example
は、テストself.execute_example
としてself.execute_example
を使用します。 たとえば、次のエグゼキューターはすべてのコードを2回実行します。
from unittest import TestCase class TestTryReallyHard(TestCase): @given(integers()) def test_something(self, i): perform_some_unreliable_operation(i) def execute_example(self, f): f() return f()
注:マップなどで使用する機能 彼らはアーティストの中で働きます。 つまり execute_example
れる関数が呼び出されるまで、それらは呼び出されません。
エグゼキュータは、転送された関数を処理できる必要があります。転送された関数はNoneを返します。そうしないと、通常のテストケースを実行できません。 たとえば、次のエグゼキューターは無効です。
from unittest import TestCase class TestRunTwice(TestCase): def execute_example(self, f): return f()()
また、次のように書き換える必要があります。
from unittest import TestCase class TestRunTwice(TestCase): def execute_example(self, f): result = f() if callable(result): result = result() return result
仮説を使用して値を見つける
仮説データマイニング関数を使用して、述語(選択条件)を満たす値を見つけることができます。 これは通常、 @composite
で定義されたカスタム戦略を検討したり、データフィルター条件を実験したりするのに@composite
ます。
hypothesis.find(指定子、条件、設定=なし、ランダム=なし、database_key =なし)
>>> from hypothesis import find >>> from hypothesis.strategies import sets, lists, integers >>> find(lists(integers()), lambda x: sum(x) >= 10) [10] >>> find(lists(integers()), lambda x: sum(x) >= 10 and len(x) >= 3) [0, 0, 10] >>> find(sets(integers()), lambda x: sum(x) >= 10 and len(x) >= 3) {0, 1, 9}
最初のhypothesis.find
引数は、 hypothesis.find
引数の通常の方法でデータを記述し、 all the same data types <data>
をサポートしall the same data types <data>
。 2番目は、満たさなければならない述語です。
もちろん、すべての条件が満たされているわけではありません。 常に偽である条件の例を仮説に求めた場合、これはエラーを投げます:
>>> find(integers(), lambda x: False) Traceback (most recent call last): ... hypothesis.errors.NoSuchExample: No examples of condition lambda x: <unknown>
( lambda x: unknown
は、仮説がPythonインタラクティブコンソールからラムダ式のソースコードを取得できないという事実によるものです。)
推定戦略
場合によっては、引数を省略した場合の仮説が仮説を立てることがあります。 これは魔法ではなく内省に基づいているため、明確に定義された制限があります。
hypothesis.strategies.builds()
target
署名hypothesis.strategies.builds()
チェックします(python3.6 inspect.getfullargspec()
)。 型注釈付きの必須引数があり、戦略がhypothesis.strategies.builds()
渡されなかった場合、 hypothesis.strategies.from_type()
使用して入力します。 特別な値hypothesis.infer()
を引数として渡して、デフォルト値の引数をこの出力にプッシュすることもできます。
>>> def func(a: int, b: str): ... return [a, b] >>> builds(func).example() [-6993, '']
@given
は、必要な引数の暗黙的な出力を@given
しません。これは、pytest機能との互換性に違反するためです。
hypothesis.infer
をキーワード引数として使用して、型注釈から引数を明示的に入力できます。
@given(a=infer) def test(a: int): pass # is equivalent to @given(a=integers()) def test(a): pass
制限事項
PEP 3107のような注釈はPython 2でサポートされておらず、仮説は実行時にPEP 484のようなコメントをチェックしません。
hypothesis.strategies.from_type
は通常どおり機能しますが、
hypothesis.strategies.builds
および@given
は、 __annotations__
属性を手動で作成する場合にのみ機能します(たとえば、 @annotations(...)
および@returns(...)
デコレーターを使用)。
バックポートがインストールされている場合、 typing
モジュールはPython 2で完全にサポートされます。
typing
モジュールは一時的なものであり、マイナーバージョンを含むPython 3.5.0と3.6.1の間で内部的な変更が多数行われています。 それらはすべてサポートされていますが、モジュールの古いバージョンで問題が発生する可能性があります。 これらについてお知らせください。回避策としてPythonの新しいバージョンへのアップグレードを検討してください。