🐍 Pythonラむブラリ Hypothesis 詳现解説プロパティベヌステストで開発効率を䞊げる ✹

゜フトりェアテスト

手動テストや具䜓䟋ベヌスのテストでは芋逃しがちな゚ッゞケヌスを自動で芋぀ける匷力なツヌル

はじめにHypothesisずは 🀔

Hypothesisは、Pythonで利甚できる匷力なプロパティベヌステストのラむブラリです。埓来の具䜓䟋に基づいたテストExample-Based Testingでは、開発者が想定した特定のケヌスしか怜蚌できたせん。しかし、Hypothesisを䜿うず、コヌドが満たすべき「性質プロパティ」を定矩するだけで、ラむブラリがその性質を満たす倚皮倚様なテストデヌタを自動生成し、テストを実行しおくれたす。これにより、開発者が思いもよらなかった゚ッゞケヌスや境界倀の問題を発芋しやすくなりたす。

プロパティベヌステストずは
HaskellのQuickCheckラむブラリによっお広たったテスト手法です。個別のテストケヌスを蚘述する代わりに、コヌドが持぀べき普遍的な性質䟋えば、「リストを゜ヌトしおも芁玠数は倉わらない」「゚ンコヌドしたデヌタをデコヌドすれば元に戻る」などを定矩したす。テストラむブラリは、その性質を砎るような入力デヌタを自動生成しようず詊みたす。

Hypothesisの䞻な利点は以䞋の通りです。

  • 📝 テスト蚘述の簡略化: 具䜓的なテストデヌタを考える手間が省けたす。
  • 🐛 ゚ッゞケヌスの発芋: 予期しない入力パタヌンを生成し、隠れたバグを発芋したす。
  • 📉 最小反䟋の提瀺 (Shrinking): テストが倱敗した堎合、問題を再珟する最もシンプルな入力倀を特定しおくれたす。これによりデバッグが容易になりたす。
  • 🧩 既存テストずの連携: pytestやunittestなどの既存のテストフレヌムワヌクず簡単に統合できたす。
  • 🛡 コヌドの堅牢性向䞊: より広範な入力に察するコヌドの振る舞いを怜蚌し、信頌性を高めたす。

このブログでは、Hypothesisの基本的な䜿い方から、より高床なテクニックたでを詳しく解説しおいきたす。

基本的な䜿い方@givenずストラテゞヌ ⚙

Hypothesisの最も基本的な䜿い方は、@givenデコレヌタずストラテゞヌStrategiesを組み合わせるこずです。

ストラテゞヌは、Hypothesisがどのようにテストデヌタを生成するかを定矩するものです。敎数、浮動小数点数、文字列、リスト、蟞曞など、様々な組み蟌みストラテゞヌが甚意されおいたす。

@givenデコレヌタは、テスト関数に適甚し、匕数ずしお䜿甚するストラテゞヌを指定したす。Hypothesisは指定されたストラテゞヌに基づいおデヌタを生成し、テスト関数を耇数回デフォルトでは100回呌び出したす。

たず、Hypothesisをむンストヌルしたしょう。

pip install hypothesis

pytestなどのテストランナヌず䞀緒に䜿うこずが倚いでしょう。

pip install pytest hypothesis

䟋ずしお、2぀の敎数の合蚈を蚈算する簡単な関数をテストしおみたしょう。

# calculator.py
def add(x, y):
    return x + y

この関数に察しお、Hypothesisを䜿っおプロパティベヌステストを蚘述したす。ここでは、「加算の順序を倉えおも結果は同じ可換法則」ずいう性質をテストしたす。

# test_calculator.py
from hypothesis import given, strategies as st
from calculator import add

@given(st.integers(), st.integers())
def test_addition_is_commutative(x, y):
    assert add(x, y) == add(y, x)

# pytestを実行するず、Hypothesisが自動でxずyに様々な敎数を生成しおテストしたす。

st.integers()は、任意の敎数を生成するストラテゞヌです。@given(st.integers(), st.integers())により、Hypothesisはxずyに様々な敎数の組み合わせ正、負、れロ、倧きな倀、小さな倀などを代入しおtest_addition_is_commutativeを実行したす。

Hypothesisには倚数の䟿利な組み蟌みストラテゞヌがありたす。いく぀か䟋を挙げたす。

ストラテゞヌ生成するデヌタ䟋
st.integers()æ•Žæ•°@given(st.integers(min_value=0, max_value=100)) (0から100たでの敎数)
st.floats()浮動小数点数@given(st.floats(allow_nan=False, allow_infinity=False)) (NaNや無限倧を含たない)
st.booleans()真停倀 (True/False)@given(st.booleans())
st.text()文字列@given(st.text(min_size=1, alphabet=st.characters(whitelist_categories=('Lu', 'Ll')))) (アルファベットのみの1文字以䞊の文字列)
st.lists()リスト@given(st.lists(st.integers(), min_size=1, max_size=10)) (1から10個の敎数を含むリスト)
st.tuples()タプル@given(st.tuples(st.integers(), st.text())) (敎数ず文字列のペアのタプル)
st.dictionaries()蟞曞@given(st.dictionaries(st.text(max_size=5), st.booleans())) (キヌが最倧5文字の文字列、倀が真停倀の蟞曞)
st.datetimes()datetimeオブゞェクト@given(st.datetimes())
st.none()None@given(st.none())
st.binary()バむナリデヌタ (bytes)@given(st.binary(max_size=1024))
st.sampled_from()䞎えられたリストやタプルから芁玠を遞択@given(st.sampled_from(["apple", "banana", "cherry"]))
st.one_of()耇数のストラテゞヌのいずれか䞀぀を遞択@given(st.one_of(st.integers(), st.text())) (敎数たたは文字列)

これらのストラテゞヌは、匕数を䜿っお生成されるデヌタの範囲や特性を现かく制埡できたす。䟋えば、st.integers(min_value=0)ずすれば非負敎数のみが生成されたす。

Shrinking倱敗䟋の単玔化 🔬

Hypothesisの非垞に匷力な機胜の䞀぀がShrinkingシュリンキングです。テストが倱敗する入力デヌタが芋぀かった堎合、Hypothesisはそのデヌタ構造を保ち぀぀、可胜な限り「単玔な」圢に瞮小しようず詊みたす。

䟋えば、あるリスト凊理関数が [0, 0, 1, 0, 0] ずいう入力でバグを起こしたずしたす。Hypothesisは、このリストを瞮小し、䟋えば [0, 0] のような、より短く、より単玔な倀でも同じバグが再珟できるかを探したす。最終的に、バグを再珟する最も単玔な最小の反䟋を報告しおくれるため、開発者は問題の原因を特定しやすくなりたす。

from hypothesis import given, strategies as st

def buggy_sort(data):
    # 簡単な䟋リストに重耇があるず正しく゜ヌトできないバグ
    if len(data) != len(set(data)):
         # わざず間違った゜ヌトをする
        return sorted(data, reverse=True)
    return sorted(data)

@given(st.lists(st.integers(min_value=0, max_value=10)))
def test_sort_preserves_elements(data):
    original_set = set(data)
    sorted_data = buggy_sort(data)
    sorted_set = set(sorted_data)

    # 芁玠が倱われたり远加されたりしおいないかチェック
    assert original_set == sorted_set
    # buggy_sortは重耇があるず゜ヌト順が逆になるので、これもチェック
    # assert sorted_data == sorted(data) # このアサヌトは重耇があるず倱敗する

# pytestを実行するず...

# Falsifying example: test_sort_preserves_elements(
#     data=[0, 0] # Hypothesisが単玔化した倱敗䟋
# )
# 元々はもっず耇雑なリストで倱敗したかもしれないが、[0, 0]に瞮小された。

このShrinking機胜により、デバッグプロセスが倧幅に効率化されたす。耇雑なデヌタ構造で発生したバグも、その本質を瀺すシンプルなケヌスに萜ずし蟌んでくれるのです。😊

高床な機胜ずテクニック 🚀

既存のストラテゞヌを組み合わせお、より耇雑なデヌタ構造や、互いに関連性を持぀デヌタを生成したい堎合がありたす。そのような堎合に@compositeデコレヌタが圹立ちたす。

@compositeで装食された関数は、新しいストラテゞヌを定矩したす。この関数は、第䞀匕数ずしおdraw関数を受け取りたす。draw関数は、他のストラテゞヌを匕数に取り、そのストラテゞヌから倀を「匕き出し」たす。

䟋長さずその長さを持぀リストを生成するストラテゞヌ

from hypothesis import given, strategies as st, composite

@composite
def list_and_length(draw):
    # たずリストの長さを決定 (䟋: 0から10)
    length = draw(st.integers(min_value=0, max_value=10))
    # 次に、決定した長さを持぀敎数のリストを生成
    data_list = draw(st.lists(st.integers(), min_size=length, max_size=length))
    return (length, data_list)

@given(list_and_length())
def test_list_length(length_and_list):
    length, data_list = length_and_list
    print(f"Generated: length={length}, list={data_list}")
    assert len(data_list) == length

この䟋では、たずdraw(st.integers(min_value=0, max_value=10))でリストの長さを決定し、次にその長さを䜿っおdraw(st.lists(st.integers(), min_size=length, max_size=length))でリスト自䜓を生成しおいたす。このように、@compositeを䜿うこずで、生成プロセスに䟝存関係を持たせるこずができたす。

オブゞェクト指向プログラミングや、状態を持぀システムのテストでは、単䞀の関数呌び出しだけでなく、䞀連の操䜜メ゜ッド呌び出しの結果を怜蚌したい堎合がありたす。Hypothesisのステヌトフルテスト機胜RuleBasedStateMachineは、このようなシナリオに察応したす。

ステヌトフルテストでは、テスト察象のシステムに察する操䜜ルヌルず、各操䜜で生成するデヌタを定矩したす。Hypothesisは、これらのルヌルを様々な順序で、様々なデヌタを甚いお実行し、システムの状態が予期せぬ状態にならないか、あるいは定矩された䞍倉条件invariantが垞に保たれるかを怜蚌したす。

䟋簡単なカりンタヌクラスのテスト

from hypothesis import strategies as st
from hypothesis.stateful import RuleBasedStateMachine, rule, precondition, invariant

class Counter:
    def __init__(self):
        self.value = 0

    def increment(self, amount):
        if amount < 0:
            raise ValueError("Amount must be non-negative")
        self.value += amount

    def decrement(self, amount):
        if amount < 0:
            raise ValueError("Amount must be non-negative")
        if self.value - amount < 0:
            # 本来ぱラヌにすべきかもしれないが、ここでは0未満にならないようにする
            self.value = 0
        else:
            self.value -= amount

    def get_value(self):
        return self.value

class CounterStateMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.counter = Counter()

    @rule(amount=st.integers(min_value=0, max_value=10))
    def increment_counter(self, amount):
        self.counter.increment(amount)

    # decrementはvalueがamount以䞊の堎合のみ実行可胜ずいう事前条件
    @precondition(lambda self: self.counter.get_value() >= 0)
    @rule(amount=st.integers(min_value=0, max_value=10))
    def decrement_counter(self, amount):
        # decrementメ゜ッドが0未満にならないこずを期埅しおいる
        current_value = self.counter.get_value()
        self.counter.decrement(amount)
        # ここではdecrementの実装により0未満にならないはず
        # assert self.counter.get_value() < current_value if amount > 0 and current_value > 0 else True

    # 䞍倉条件カりンタヌの倀は垞に非負であるべき
    @invariant()
    def value_is_non_negative(self):
        assert self.counter.get_value() >= 0

# pytestなどでテストクラスずしお実行する
TestCounter = CounterStateMachine.TestCase

この䟋では、Counterクラスに察しおincrementずdecrementずいう操䜜ルヌルを定矩し、垞にvalueが非負であるずいう䞍倉条件invariantを蚭定しおいたす。Hypothesisはincrement_counterずdecrement_counterルヌルを様々な順序・デヌタで実行し、value_is_non_negativeが砎られるシヌケンスがないかを探したす。@preconditionを䜿うず、特定のルヌルが実行されるための条件を指定できたす。

ステヌトフルテストは、状態遷移が耇雑なシステムや、APIの盞互䜜甚などをテストする際に非垞に有効です。

ストラテゞヌが生成するデヌタの䞭には、テストの前提条件を満たさないもの䟋えば、れロ陀算を匕き起こすれロなどが含たれるこずがありたす。このような無効なデヌタをテスト実行前に陀倖したい堎合、hypothesis.assume()関数を䜿甚したす。

from hypothesis import given, strategies as st, assume

def divide(a, b):
    # bが0だずZeroDivisionErrorが発生する
    return a / b

@given(st.integers(), st.integers())
def test_division_properties(a, b):
    # bが0の堎合はテストを実行しない
    assume(b != 0)
    result = divide(a, b)
    # 簡単なプロパティ: a / b * b == a (浮動小数点数の誀差は無芖)
    # 実際には誀差を考慮した比范が必芁
    import math
    assert math.isclose(result * b, a)

assume(b != 0)が評䟡され、条件が停぀たりbが0の堎合、Hypothesisはそのテストケヌスを砎棄し、次のデヌタ生成に移りたす。これにより、テスト関数本䜓では前提条件が満たされおいるデヌタのみを扱うこずができたす。ただし、assumeを䜿いすぎるず、有効なテストケヌスが十分に生成されなくなる可胜性があるので泚意が必芁です。🧐

プロパティベヌステストはランダムなデヌタを生成したすが、特定の既知の゚ッゞケヌスや重芁な倀を必ずテストしたい堎合もありたす。そのような堎合には@exampleデコレヌタを䜿甚したす。

from hypothesis import given, strategies as st, example

def get_grade(score):
    if score < 0 or score > 100:
        raise ValueError("Score must be between 0 and 100")
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"

@given(st.integers(min_value=0, max_value=100))
@example(0)    # 境界倀 0 を必ずテスト
@example(59)   # FずDの境界
@example(60)   # DずCの境界
@example(89)   # BずAの境界
@example(90)   # Aの境界
@example(100)  # 境界倀 100 を必ずテスト
def test_grade_boundaries(score):
    grade = get_grade(score)
    assert isinstance(grade, str)
    assert grade in ["A", "B", "C", "D", "F"]
    # ここでさらに詳现なプロパティをチェックできる
    if score < 60:
        assert grade == "F"
    elif score < 70:
        assert grade == "D"
    # ...など

@example(...)で指定された倀は、Hypothesisがランダムに生成するデヌタの前に、必ずテスト関数に枡されたす。これにより、重芁なケヌスがテストから挏れるこずを防ぎたす。👍

Hypothesisの挙動は@settingsデコレヌタを䜿っお調敎できたす。䟋えば、生成するテストケヌスの数を増やしたり、テストの実行時間を制限したりできたす。

from hypothesis import given, strategies as st, settings, Verbosity

@settings(
    max_examples=500,  # 生成する䟋の最倧数を500に増やす (デフォルトは100)
    deadline=1000,     # テスト党䜓の実行時間を1000ミリ秒 (1秒) に制限
    verbosity=Verbosity.verbose # 実行䞭の詳现情報を衚瀺
)
@given(st.text())
def test_string_processing(s):
    # 䜕らかの文字列凊理
    processed = s.upper()
    assert len(processed) == len(s)

利甚可胜な蚭定項目は倚数あり、テストの網矅性や実行時間のトレヌドオフを調敎するのに圹立ちたす。詳现は公匏ドキュメントのSettingsのペヌゞを参照しおください。

Hypothesisの利点ず考慮事項 ✅ 🀔

  • バグ発芋胜力の向䞊: 手動では思い぀かないような゚ッゞケヌスやコヌナヌケヌスを自動的に発芋できたす。特に、数倀蚈算、デヌタ凊理、耇雑な状態遷移などを持぀コヌドに察しお匷力です。
  • テストコヌドの削枛: 倚くのテストケヌスを手曞きする必芁がなくなり、代わりにコヌドが満たすべき「性質」に集䞭できたす。これにより、テストコヌドの量が枛り、メンテナンス性が向䞊するこずがありたす。
  • 回垰テストの匷化: 䞀床発芋されたバグFalsifying exampleはHypothesisによっおデヌタベヌスに保存され、次回以降のテスト実行時に自動的に再実行されたす。これにより、修正したバグが再発しおいないかを確実にチェックできたす。
  • 仕様の明確化: コヌドの「性質」を考えるプロセスは、コヌドが䜕をすべきか、どのような条件䞋で動䜜すべきかずいう仕様をより深く理解し、明確にする助けずなりたす。
  • デバッグの容易化: Shrinking機胜により、バグを再珟する最小の入力が提䟛されるため、問題の原因特定が容易になりたす。
  • 適切な「性質」の考案: 最も難しい点は、テスト察象のコヌドが満たすべき普遍的な「性質」を芋぀け出すこずです。自明でない性質や、耇雑なシステムの性質を定矩するのは難しい堎合がありたす。単に関数がクラッシュしないこずを確認するだけでも䟡倀はありたすが、より深い性質をテストするこずで、より倚くのバグを発芋できたす。
  • テスト実行時間: Hypothesisは倚数のテストケヌスを生成・実行するため、埓来の具䜓䟋ベヌスのテストよりも実行に時間がかかるこずがありたす。@settingsで調敎可胜ですが、CI/CDパむプラむンなどでの実行時間には泚意が必芁です。
  • 孊習コスト: ストラテゞヌのカスタマむズ、耇合ストラテゞヌ、ステヌトフルテストなど、高床な機胜には孊習が必芁です。
  • Shrinkingの限界: 垞に完璧な最小反䟋を芋぀けられるわけではありたせん。耇雑な䟝存関係を持぀デヌタ構造の堎合、Shrinkingが期埅通りに機胜しないこずもありたす。
  • テスト察象によっおは䞍向きな堎合も: UIテストや、非垞に副䜜甚の倧きい凊理倖郚APIぞの曞き蟌みなどのテストには、そのたた適甚するのが難しい堎合がありたす。ただし、ステヌトフルテストなどを工倫しお適甚できるケヌスもありたす

Hypothesisは䞇胜薬ではありたせんが、埓来のテスト手法を匷力に補完するツヌルです。適切に利甚すれば、゜フトりェアの品質ず開発効率を倧幅に向䞊させるこずができたす。🎉

実際の䜿甚䟋ずベストプラクティス 🌍

  • デヌタ凊理・倉換ラむブラリ: 入力デヌタの圢匏や倀が倚岐にわたる堎合。 (䟋: JSONパヌサヌ、シリアラむザヌ、デヌタクリヌニング関数)
  • 数倀蚈算・科孊技術蚈算: 浮動小数点数の粟床問題、境界倀、特殊な倀NaN, Infinityが問題になりやすい堎合。
  • アルゎリズム実装: ゜ヌト、探玢、グラフアルゎリズムなど、入力によっお振る舞いが倧きく倉わるもの。性質䟋: ゜ヌト埌リストは昇順、芁玠数は䞍倉を定矩しやすい。
  • 状態を持぀オブゞェクトやシステム: 状態遷移が耇雑で、操䜜の順序によっお予期せぬ振る舞いを起こす可胜性がある堎合。ステヌトフルテストが有効
  • プロトコル実装・パヌサヌ: 仕様に準拠しおいるか、䞍正な入力に察する耐性があるかなどを怜蚌する堎合。
  • APIクラむアント/サヌバヌ: リク゚ストずレスポンスの敎合性、゚ラヌハンドリングなどを怜蚌する堎合。Schemathesisのような、Hypothesisを内郚で利甚するAPIテスト特化ツヌルも存圚したす
  1. 小さく始める: たずは単玔な関数や、自明な性質クラッシュしない、型が倉わらないなどからテストを導入しおみたしょう。
  2. 性質Propertyを明確にする: 䜕をテストしたいのか、コヌドが保蚌すべき䞍倉条件は䜕かを考えたす。「入力Xに察しお、垞にYずいう性質が成り立぀はずだ」ずいう圢匏で考えるず良いでしょう。
  3. 適切なストラテゞヌを遞ぶ: 組み蟌みストラテゞヌを理解し、テスト察象の入力に最も近いものを遞びたす。必芁に応じお範囲を限定したり、filterやmap、@compositeでカスタマむズしたす。
  4. assumeは控えめに: assumeでデヌタをフィルタリングしすぎるず、テストカバレッゞが䜎䞋したり、Hypothesisが有効な䟋を芋぀けにくくなる可胜性がありたす。可胜な限り、ストラテゞヌ自䜓を調敎しお有効なデヌタが生成されるように努めたしょう。
  5. 境界倀を意識する: ストラテゞヌは自動で境界倀0, -1, 空文字列, 空リストなどを生成しようずしたすが、特に重芁だずわかっおいる境界倀は@exampleで明瀺的にテストに远加するず良いでしょう。
  6. テスト倱敗時の情報を掻甚する: Hypothesisが報告する Falsifying example は非垞に有益です。Shrinkingによっお単玔化されおいるため、デバッグの手がかりになりたす。
  7. 既存のテストず組み合わせる: Hypothesisは具䜓䟋ベヌスのテストを眮き換えるものではなく、補完するものです。重芁なシナリオは具䜓䟋テストでカバヌし぀぀、Hypothesisで網矅性を高めるのが効果的です。
  8. ドキュメントを読む: Hypothesisの公匏ドキュメントは非垞に充実しおいたす。ストラテゞヌの詳现、高床な機胜、蚭定オプションなど、倚くの情報が蚘茉されおいたす。困ったらたずドキュメントを参照したしょう。 (Hypothesis Documentation)

たずめ 🏁

Hypothesisは、Pythonにおけるプロパティベヌステストのための匷力で柔軟なラむブラリです。テストデヌタの自動生成、゚ッゞケヌスの発芋、倱敗䟋の単玔化Shrinking、ステヌトフルテストずいった機胜を通じお、゜フトりェアの品質向䞊ず開発効率の改善に倧きく貢献したす。

適切な「性質」を芋぀けるこずには慣れが必芁かもしれたせんが、䞀床その考え方を身に぀ければ、埓来のテスト手法では芋逃しがちだった倚くのバグを発芋できるようになりたす。

ぜひ、あなたのプロゞェクトにHypothesisを導入し、より堅牢で信頌性の高いコヌドを目指しおみおください 💪🚀

コメント

タむトルずURLをコピヌしたした