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()