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]:
import re

from hypothesis import assume, given
from hypothesis.strategies import emails, integers, tuples

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()
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[4], line 1
----> 1 test_calculate_range()

Cell In[3], line 2, 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)

    [... skipping hidden 1 frame]

Cell In[3], line 5, in test_calculate_range(tup)
      3 result = calculate_range(tup)
      4 assert isinstance(result, int)
----> 5 assert result > 0

AssertionError:
Falsifying example: test_calculate_range(
    tup=(0, 0, 0),
)

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()
---------------------------------------------------------------------------
ExceptionGroup                            Traceback (most recent call last)
Cell In[9], line 1
----> 1 test_parse_email()

Cell In[8], line 2, in test_parse_email()
      1 @given(emails())
----> 2 def test_parse_email(email):
      3     result = parse_email(email)
      4     # print(result)

File ~/.local/share/virtualenvs/python-311-6zxVKbDJ/lib/python3.11/site-packages/hypothesis/core.py:1374, in given.<locals>.run_test_as_given.<locals>.wrapped_test(*arguments, **kwargs)
   1361         # The dance here is to avoid showing users long tracebacks
   1362         # full of Hypothesis internals they don't care about.
   1363         # We have to do this inline, to avoid adding another
   (...)
   1367         # which will actually appear in tracebacks is as clear as
   1368         # possible - "raise the_error_hypothesis_found".
   1369         the_error_hypothesis_found = e.with_traceback(
   1370             None
   1371             if isinstance(e, BaseExceptionGroup)
   1372             else get_trimmed_traceback()
   1373         )
-> 1374         raise the_error_hypothesis_found
   1376 if not (ran_explicit_examples or state.ever_executed):
   1377     raise SKIP_BECAUSE_NO_EXAMPLES

ExceptionGroup: Hypothesis found 2 distinct failures. (2 sub-exceptions)

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