Hypothesis: Property -basiertes Testen#
In diesem Notebook verwenden wir Property -basierte Tests, um Probleme in unserem Code zu finden. Hypothesis ist eine Bibliothek, die Haskells Quickcheck ähnelt. Später lernen wir sie zusammen mit anderen Testbibliotheken noch genauer kennen: Hypothesis. Hypothesis kann auch Mock-Objekte und Tests für Numpy-Datentypen bereitstellen.
1. Importe#
[1]:
from hypothesis import given, assume
from hypothesis.strategies import tuples, integers, emails
import re
2. Bereich finden#
[2]:
def calculate_range(tuple_obj):
return max(tuple_obj) - min(tuple_obj)
3. Test mit strategies
und given
#
Mit hypothesis.strategies könnt ihr unterschiedliche Testdaten erstellen. Hierfür beitet Hypothesis Strategien für die meisten Typen und Argumente schränken die Möglichkeiten ein um sie euren Erfordernissen anzupassen. Im Beispiel unten verwenden wir die integers-Strategie, die mit dem
Python-Decorator @given
auf die Funktion angewendet wird. Genauer nimmt er unsere Testfunktion und wandelt sie in eine parametrisierte um sie über weite Bereiche passender Daten auszuführen:
[3]:
@given(tuples(integers(), integers(), integers()))
def test_calculate_range(tup):
result = calculate_range(tup)
assert isinstance(result, int)
assert result > 0
[4]:
test_calculate_range()
Falsifying example: test_calculate_range(
tup=(0, 0, 0),
)
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/3860635982.py in <module>
----> 1 test_calculate_range()
/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/2632287104.py in test_calculate_range()
1 @given(tuples(integers(), integers(), integers()))
----> 2 def test_calculate_range(tup):
3 result = calculate_range(tup)
4 assert isinstance(result, int)
5 assert result > 0
[... skipping hidden 1 frame]
/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/2632287104.py in test_calculate_range(tup)
3 result = calculate_range(tup)
4 assert isinstance(result, int)
----> 5 assert result > 0
AssertionError:
Nun korrigieren wir den Test mit >=
und überprüfen ihn erneut:
[5]:
@given(tuples(integers(), integers()))
def test_calculate_range(tup):
result = calculate_range(tup)
assert isinstance(result, int)
assert result >= 0
[6]:
test_calculate_range()
3. Gegen Reguläre Ausdrücke prüfen#
Mit regulären Ausrücken (engl.: regular expressions) lassen sich Zeichenketten auf bestimmte syntaktische Regeln überprüfen. In Python könnt ihr zum Überprüfen regulärer Ausdrücke re.match verwenden.
Hinweis:
Auf der Website regex101 könnt ihr zunächst eure regulären Ausdrücke ausprobieren.
Als Beispiel versuchen wir, aus E-Mail-Adressen username
und die domain
zu ermitteln:
[7]:
def parse_email(email):
result = re.match('(?P<username>\w+).(?P<domain>[\w\.]+)',
email).groups()
return result
Nun schreiben wir einen Test test_parse_email
zum Überprüfen unserer Methode. Als Eingabewerte verwenden wir die emails-Strategie von Hypothesis. Als result
erwarten wir z.B.:
('0', 'A.com')
('F', 'j.EeHNqsx')
…
Im Test nehmen wir einerseits an, dass immer zwei Einträge zurückgegeben werden und im zweiten Eintrag ein Punkt (.
) vorkommt.
[8]:
@given(emails())
def test_parse_email(email):
result = parse_email(email)
# print(result)
assert len(result) == 2
assert '.' in result[1]
[9]:
test_parse_email()
Falsifying example: test_parse_email(
email='0/0@A.ac',
)
Traceback (most recent call last):
File "/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/3430727167.py", line 6, in test_parse_email
assert '.' in result[1]
AssertionError
Falsifying example: test_parse_email(
email='/@A.ac',
)
Traceback (most recent call last):
File "/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/3430727167.py", line 3, in test_parse_email
result = parse_email(email)
File "/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/1691707279.py", line 2, in parse_email
result = re.match('(?P<username>\w+).(?P<domain>[\w\.]+)',
AttributeError: 'NoneType' object has no attribute 'groups'
---------------------------------------------------------------------------
MultipleFailures Traceback (most recent call last)
/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/3120039226.py in <module>
----> 1 test_parse_email()
/var/folders/f8/0034db6d78s5r6m34fxhpk7m0000gp/T/ipykernel_2176/3430727167.py in test_parse_email()
1 @given(emails())
----> 2 def test_parse_email(email):
3 result = parse_email(email)
4 # print(result)
5 assert len(result) == 2
[... skipping hidden 1 frame]
~/cusy/trn/jupyter-tutorial-de/lib/python3.9/site-packages/hypothesis/core.py in run_engine(self)
885 )
886 else:
--> 887 raise MultipleFailures(
888 f"Hypothesis found {len(self.falsifying_examples)} distinct failures."
889 )
MultipleFailures: Hypothesis found 2 distinct failures.
Mit Hypothesis wurden zwei Beispiele gefunden, die deutlich machen, dass unser regulärer Ausdruck in der parse_email
-Methode noch nicht hinreichend ist: 0/0@A.ac
und /@A.ac
. Nachdem wir unseren regulären Ausdruck entsprechend angepasst haben, können wir den Test erneut aufrufen:
[10]:
def parse_email(email):
result = re.match('(?P<username>[\.\w\-\!~#$%&\|{}\+\/\^\`\=\*\']+).(?P<domain>[\w\.\-]+)', email).groups()
return result
[11]:
test_parse_email()