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:
- 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¶
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
Für unseren Test verwenden wir nun
hypothesis.givenals 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)
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 Mittelwertsberechnunginfundinfist nicht kleiner als die größere der beiden Zahlen.
Beispiel mit regulären Ausdrücken¶
Im folgenden Beispiel versuchen wir, aus einer E-Mail-Adresse
usernameunddomainmit 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
Nun schreiben wir einen Test
test_parse_email()zum Überprüfen unserer Funktion. Als Eingabewerte verwenden wir die emails-Strategie von Hypothesis. Alsresulterwarten wir z. B. beiveit@cusy.ioalsusernameveit und alsdomaincusy.io.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]
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.acverdeutlicht, dass unser regulärer Ausdruck in derparse_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.