Hypothesis

Property Testing ist eine Testmethode, bei der nicht überprüft wird, ob bestimmte Eingaben zu bestimmten Ausgaben führen, sondern bei der zufällig Eingaben generiert werden, uns das Programm mit allen diesen Eingaben ausgeführtund dann die Gültigkeit dieser Eigenschaft überprüft wird.

In Python könnt ihr euch mit Hypothesis solche Eingaben parametrisiert generieren lassen, um schnell Fehler in euren Tests finden zu können.

Siehe auch

Im Jupyter Tutorial ist beschrieben, wie Hypothesis auch in Jupyter Notebooks verwendet werden kann.

Bemerkung

Auch bei agentischer Software-Entwicklung verwenden wir Hypothesis:

AGENTS.md
- Use the `hypothesis` library for property-based testing when you have complex input spaces or need to test edge cases.

Siehe auch

Installation

$ uv add --group tests hypothesis
C:> uv add --group tests hypothesis

Alternativ kann Hypothesis auch mit Erweiterungen installiert werden, z. B.:

$ uv add --group tests "hypothesis[numpy, pandas]"
C:> uv add --group tests "hypothesis[numpy, pandas]"

Beispiel mit strategies und given

  1. Zunächst importieren wir von hypothesis.strategies Beispieldaten für floats und lists. Um diese Beispieldaten auf unsere Testfunktion anwenden zu können, importieren wir darüberhinaus hypothesis.given:

    1import pytest
    2from hypothesis import given
    3from hypothesis.strategies import floats, lists
    
  2. Für unseren Test verwenden wir nun hypothesis.given als Dekorator um die Testfunktion in eine parametrisierte umzuwandeln, die dann mit einer großen Varianz passender Daten ausgeführt wird:

    6@given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1))
    7def test_mean(ls):
    8    mean = sum(ls) / len(ls)
    9    assert min(ls) <= mean <= max(ls)
    
  3. Schließlich führen wir den Test aus:

    $ uv run pytest docs/test/test_hypothesis.py
    ============================= test session starts ==============================
    platform darwin -- Python 3.14.0, pytest-9.0.3, pluggy-1.6.0
    rootdir: /Users/veit/cusy/trn/python-basics-tutorial-de
    plugins: hypothesis-6.152.1
    collected 1 item
    
    test_hypothesis.py F                                                     [100%]
    
    =================================== FAILURES ===================================
    __________________________________ test_mean ___________________________________
    
        @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1))
    >   def test_mean(ls):
    
    test_hypothesis.py:6:
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    
    ls = [9.9792015476736e+291, 1.7976931348623157e+308]
    
        @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1))
        def test_mean(ls):
            mean = sum(ls) / len(ls)
    >       assert min(ls) <= mean <= max(ls)
    E       assert inf <= 1.7976931348623157e+308
    E         +  where 1.7976931348623157e+308 = max([9.9792015476736e+291, 1.7976931348623157e+308])
    
    test_hypothesis.py:8: AssertionError
    ---------------------------------- Hypothesis ----------------------------------
    Falsifying example: test_mean(
        ls=[9.9792015476736e+291, 1.7976931348623157e+308],
    )
    =========================== short test summary info ============================
    FAILED test_hypothesis.py::test_mean - assert inf <= 1.7976931348623157e+308
    ============================== 1 failed in 0.44s ===============================
    
    C:> uv run pytest docs/test/test_hypothesis.py
    ============================= test session starts ==============================
    platform darwin -- Python 3.14.0, pytest-9.0.3, pluggy-1.6.0
    rootdir: C:\Users\veit\python-basics-tutorial-de
    plugins: hypothesis-6.152.1
    collected 1 item
    
    test_hypothesis.py F                                                     [100%]
    
    =================================== FAILURES ===================================
    __________________________________ test_mean ___________________________________
    
        @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1))
    >   def test_mean(ls):
    
    test_hypothesis.py:6:
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    
    ls = [9.9792015476736e+291, 1.7976931348623157e+308]
    
        @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1))
        def test_mean(ls):
            mean = sum(ls) / len(ls)
    >       assert min(ls) <= mean <= max(ls)
    E       assert inf <= 1.7976931348623157e+308
    E        +  where 1.7976931348623157e+308 = max([9.9792015476736e+291, 1.7976931348623157e+308])
    
    test_hypothesis.py:8: AssertionError
    ---------------------------------- Hypothesis ----------------------------------
    Falsifying example: test_mean(
        ls=[9.9792015476736e+291, 1.7976931348623157e+308],
    )
    =========================== short test summary info ============================
    FAILED test_hypothesis.py::test_mean - assert inf <= 1.7976931348623157e+308
    ============================== 1 failed in 0.44s ===============================
    

    In der Liste [9.9792015476736e+291, 1.7976931348623157e+308] ergibt die Mittelwertsberechnung inf und inf ist nicht kleiner als die größere der beiden Zahlen.

Beispiel mit regulären Ausdrücken

  1. Im folgenden Beispiel versuchen wir, aus einer E-Mail-Adresse username und domain mit einem regulären Ausdruck zu ermitteln:

    import re
    
    
    def parse_email(email):
        result = re.match(r"(?P<username>\w+).(?P<domain>[\w\.]+)", email).groups()
        return result
    
  2. Nun schreiben wir einen Test test_parse_email() zum Überprüfen unserer Funktion. Als Eingabewerte verwenden wir die emails-Strategie von Hypothesis. Als result erwarten wir z. B. bei veit@cusy.io als username veit und als domain cusy.io.

  3. In unserem Test nehmen wir einerseits an, dass immer zwei Einträge zurückgegeben werden und im zweiten Eintrag ein Punkt (.) vorkommt:

    from hypothesis import given
    from hypothesis.strategies import emails
    
    
    @given(emails())
    def test_parse_email(email):
        result = parse_email(email)
        # print(result)
        assert len(result) == 2
        assert "." in result[1]
    
  4. Nun führen wir den Test aus:

    $ uv run pytest docs/test/test_emails.py
    ============================= test session starts ==============================
    platform darwin -- Python 3.14.0, pytest-9.0.3, pluggy-1.6.0
    rootdir: /Users/veit/cusy/trn/python-basics-tutorial-de
    configfile: pyproject.toml
    plugins: hypothesis-6.152.1
    collected 1 item
    
    docs/test/test_emails.py F                                               [100%]
    
    =================================== FAILURES ===================================
    _______________________________ test_parse_email _______________________________
      + Exception Group Traceback (most recent call last):
      |   File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 12, in test_parse_email
      |     def test_parse_email(email):
      |                    ^^^
      |   File "/Users/veit/cusy/trn/python-basics-tutorial-de/.venv/lib/python3.14/site-packages/hypothesis/core.py", line 2264, in wrapped_test
      |     raise the_error_hypothesis_found
      | ExceptionGroup: Hypothesis found 2 distinct failures. (2 sub-exceptions)
      +-+---------------- 1 ----------------
        | Traceback (most recent call last):
        |   File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 16, in test_parse_email
        |     assert '.' in result[1]
        | AssertionError: assert '.' in '0'
        | Falsifying example: test_parse_email(
        |     email='0/0@A.AC',
        | )
        +---------------- 2 ----------------
        | Traceback (most recent call last):
        |   File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 13, in test_parse_email
        |     result = parse_email(email)
        |   File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 8, in parse_email
        |     result = re.match(r"(?P<username>\w+).(?P<domain>[\w\.]+)", email).groups()
        |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        | AttributeError: 'NoneType' object has no attribute 'groups'
        | Falsifying example: test_parse_email(
        |     email='/@A.AC',
        | )
        +------------------------------------
    =========================== short test summary info ============================
    FAILED docs/test/test_emails.py::test_parse_email - ExceptionGroup: Hypothesis found 2 distinct failures. (2 sub-exceptions)
    ============================== 1 failed in 0.14s ===============================
    

    Das von Hypothesis gegebene E-Mail-Adresse 0/0@A.ac verdeutlicht, dass unser regulärer Ausdruck in der parse_email()-Methode noch nicht hinreichend ist. Daher passen wir nun unseren regulären Ausdruck an und rufen anschließend den Test erneut auf:

    --- /home/docs/checkouts/readthedocs.org/user_builds/python-basics-tutorial-de/checkouts/latest/docs/test/test_emails.py
    +++ /home/docs/checkouts/readthedocs.org/user_builds/python-basics-tutorial-de/checkouts/latest/docs/test/test_emails_2.py
    @@ -5,7 +5,9 @@
     
     
     def parse_email(email):
    -    result = re.match(r"(?P<username>\w+).(?P<domain>[\w\.]+)", email).groups()
    +    result = re.match(
    +        r"(?P<username>[\.\w\-\!~#$%&\|{}\+\/\^\`\=\*']+).(?P<domain>[\w\.\-]+)", email
    +    ).groups()
         return result
     
     
    
    $ uv run pytest docs/test/test_emails_2.py
    ============================= test session starts ==============================
    platform darwin -- Python 3.14.0, pytest-9.0.3, pluggy-1.6.0
    rootdir: /Users/veit/cusy/trn/python-basics-tutorial-de
    configfile: pyproject.toml
    plugins: hypothesis-6.152.1
    collected 1 item
    
    docs/test/test_emails_2.py .                                             [100%]
    
    ============================== 1 passed in 0.29s ===============================
    

Erweiterungen von Drittanbietern

Es gibt eine Reihe von Open-Source-Bibliotheken, die Testen erweitern. Auf Third-party extensions sind einige davon aufgeführt; weitere findet ihr auf PyPI, wenn ihr nach Stichworten oder nach Framework-Classifier durchsucht.