{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Hypothesis: _Property_ -basiertes Testen\n", "\n", "In diesem Notebook verwenden wir _Property_ -basierte Tests, um Probleme in unserem Code zu finden. [Hypothesis](https://hypothesis.readthedocs.io/en/latest/) ist eine Bibliothek, die Haskells [Quickcheck](https://hackage.haskell.org/package/QuickCheck) ähnelt. Später lernen wir sie zusammen mit anderen Testbibliotheken noch genauer kennen: [Hypothesis](https://jupyter-tutorial.readthedocs.io/de/latest/notebook/testing/hypothesis.html). Hypothesis kann auch Mock-Objekte und Tests für Numpy-Datentypen bereitstellen." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Importe" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:23.909577Z", "iopub.status.busy": "2026-05-22T13:50:23.909350Z", "iopub.status.idle": "2026-05-22T13:50:23.967124Z", "shell.execute_reply": "2026-05-22T13:50:23.966768Z", "shell.execute_reply.started": "2026-05-22T13:50:23.909554Z" } }, "outputs": [], "source": [ "import re\n", "\n", "from hypothesis import given\n", "from hypothesis.strategies import emails, integers, tuples" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Bereich finden" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:23.967672Z", "iopub.status.busy": "2026-05-22T13:50:23.967576Z", "iopub.status.idle": "2026-05-22T13:50:23.970236Z", "shell.execute_reply": "2026-05-22T13:50:23.969888Z", "shell.execute_reply.started": "2026-05-22T13:50:23.967664Z" } }, "outputs": [], "source": [ "def calculate_range(tuple_obj):\n", " return max(tuple_obj) - min(tuple_obj)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Test mit `strategies` und `given`\n", "\n", "Mit [hypothesis.strategies](https://hypothesis.readthedocs.io/en/latest/data.html) 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](https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.integers)-Strategie, die mit dem [Python-Decorator](https://docs.python.org/3/glossary.html#term-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:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:23.970751Z", "iopub.status.busy": "2026-05-22T13:50:23.970625Z", "iopub.status.idle": "2026-05-22T13:50:23.973267Z", "shell.execute_reply": "2026-05-22T13:50:23.972877Z", "shell.execute_reply.started": "2026-05-22T13:50:23.970740Z" } }, "outputs": [], "source": [ "@given(tuples(integers(), integers(), integers()))\n", "def test_calculate_range(tup):\n", " result = calculate_range(tup)\n", " assert isinstance(result, int)\n", " assert result > 0" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:23.973854Z", "iopub.status.busy": "2026-05-22T13:50:23.973733Z", "iopub.status.idle": "2026-05-22T13:50:24.194770Z", "shell.execute_reply": "2026-05-22T13:50:24.193640Z", "shell.execute_reply.started": "2026-05-22T13:50:23.973843Z" } }, "outputs": [ { "ename": "AssertionError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[4], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtest_calculate_range\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", "Cell \u001b[0;32mIn[3], line 2\u001b[0m, in \u001b[0;36mtest_calculate_range\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;129m@given\u001b[39m(tuples(integers(), integers(), integers()))\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtest_calculate_range\u001b[39m(tup):\n\u001b[1;32m 3\u001b[0m result \u001b[38;5;241m=\u001b[39m calculate_range(tup)\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(result, \u001b[38;5;28mint\u001b[39m)\n", " \u001b[0;31m[... skipping hidden 1 frame]\u001b[0m\n", "Cell \u001b[0;32mIn[3], line 5\u001b[0m, in \u001b[0;36mtest_calculate_range\u001b[0;34m(tup)\u001b[0m\n\u001b[1;32m 3\u001b[0m result \u001b[38;5;241m=\u001b[39m calculate_range(tup)\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(result, \u001b[38;5;28mint\u001b[39m)\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m result \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m0\u001b[39m\n", "\u001b[0;31mAssertionError\u001b[0m: ", "\u001b[0mFalsifying example: test_calculate_range(\n tup=(0, 0, 0),\n)" ] } ], "source": [ "test_calculate_range()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nun korrigieren wir den Test mit `>=` und überprüfen ihn erneut:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:30.121248Z", "iopub.status.busy": "2026-05-22T13:50:30.120580Z", "iopub.status.idle": "2026-05-22T13:50:30.126247Z", "shell.execute_reply": "2026-05-22T13:50:30.125239Z", "shell.execute_reply.started": "2026-05-22T13:50:30.121190Z" } }, "outputs": [], "source": [ "@given(tuples(integers(), integers()))\n", "def test_calculate_range(tup):\n", " result = calculate_range(tup)\n", " assert isinstance(result, int)\n", " assert result >= 0" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:30.127878Z", "iopub.status.busy": "2026-05-22T13:50:30.127641Z", "iopub.status.idle": "2026-05-22T13:50:30.175103Z", "shell.execute_reply": "2026-05-22T13:50:30.174607Z", "shell.execute_reply.started": "2026-05-22T13:50:30.127854Z" } }, "outputs": [], "source": [ "test_calculate_range()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Gegen Reguläre Ausdrücke prüfen\n", "\n", "Mit [regulären Ausrücken](https://de.wikipedia.org/wiki/Regul%C3%A4rer_Ausdruck) (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](https://docs.python.org/3/library/re.html#re.match) verwenden.\n", "\n", "> **Hinweis:**\n", "> \n", "> Auf der Website [regex101](https://regex101.com/) könnt ihr zunächst eure regulären Ausdrücke ausprobieren.\n", "\n", "Als Beispiel versuchen wir, aus E-Mail-Adressen `username` und die `domain` zu ermitteln:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:30.175608Z", "iopub.status.busy": "2026-05-22T13:50:30.175506Z", "iopub.status.idle": "2026-05-22T13:50:30.178369Z", "shell.execute_reply": "2026-05-22T13:50:30.178005Z", "shell.execute_reply.started": "2026-05-22T13:50:30.175598Z" } }, "outputs": [], "source": [ "def parse_email(email):\n", " result = re.match(\n", " r\"(?P\\w+).(?P[\\w\\.]+)\",\n", " email,\n", " ).groups()\n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nun schreiben wir einen Test `test_parse_email` zum Überprüfen unserer Methode. Als Eingabewerte verwenden wir die [emails](https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.emails)-Strategie von Hypothesis. Als `result` erwarten wir z.B.:\n", "```\n", "('0', 'A.com')\n", "('F', 'j.EeHNqsx')\n", "…\n", "```\n", "Im Test nehmen wir einerseits an, dass immer zwei Einträge zurückgegeben werden und im zweiten Eintrag ein Punkt (`.`) vorkommt. " ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:30.179911Z", "iopub.status.busy": "2026-05-22T13:50:30.179754Z", "iopub.status.idle": "2026-05-22T13:50:30.182159Z", "shell.execute_reply": "2026-05-22T13:50:30.181900Z", "shell.execute_reply.started": "2026-05-22T13:50:30.179894Z" } }, "outputs": [], "source": [ "@given(emails())\n", "def test_parse_email(email):\n", " result = parse_email(email)\n", " assert len(result) == 2\n", " assert \".\" in result[1]" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:30.182596Z", "iopub.status.busy": "2026-05-22T13:50:30.182486Z", "iopub.status.idle": "2026-05-22T13:50:30.545308Z", "shell.execute_reply": "2026-05-22T13:50:30.544631Z", "shell.execute_reply.started": "2026-05-22T13:50:30.182586Z" } }, "outputs": [ { "ename": "AttributeError", "evalue": "'NoneType' object has no attribute 'groups'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[9], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtest_parse_email\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", "Cell \u001b[0;32mIn[8], line 2\u001b[0m, in \u001b[0;36mtest_parse_email\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;129m@given\u001b[39m(emails())\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtest_parse_email\u001b[39m(email):\n\u001b[1;32m 3\u001b[0m result \u001b[38;5;241m=\u001b[39m parse_email(email)\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(result) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m2\u001b[39m\n", " \u001b[0;31m[... skipping hidden 1 frame]\u001b[0m\n", "Cell \u001b[0;32mIn[8], line 3\u001b[0m, in \u001b[0;36mtest_parse_email\u001b[0;34m(email)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;129m@given\u001b[39m(emails())\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtest_parse_email\u001b[39m(email):\n\u001b[0;32m----> 3\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mparse_email\u001b[49m\u001b[43m(\u001b[49m\u001b[43memail\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(result) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m2\u001b[39m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m.\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m result[\u001b[38;5;241m1\u001b[39m]\n", "Cell \u001b[0;32mIn[7], line 5\u001b[0m, in \u001b[0;36mparse_email\u001b[0;34m(email)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mparse_email\u001b[39m(email):\n\u001b[1;32m 2\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mre\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmatch\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43mr\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m(?P\u001b[39;49m\u001b[38;5;124;43m\\\u001b[39;49m\u001b[38;5;124;43mw+).(?P[\u001b[39;49m\u001b[38;5;124;43m\\\u001b[39;49m\u001b[38;5;124;43mw\u001b[39;49m\u001b[38;5;124;43m\\\u001b[39;49m\u001b[38;5;124;43m.]+)\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43memail\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m----> 5\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroups\u001b[49m()\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", "\u001b[0;31mAttributeError\u001b[0m: 'NoneType' object has no attribute 'groups'", "\u001b[0mFalsifying example: test_parse_email(\n email='/@A.ac',\n)" ] } ], "source": [ "test_parse_email()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:35.724521Z", "iopub.status.busy": "2026-05-22T13:50:35.723789Z", "iopub.status.idle": "2026-05-22T13:50:35.730977Z", "shell.execute_reply": "2026-05-22T13:50:35.729993Z", "shell.execute_reply.started": "2026-05-22T13:50:35.724459Z" } }, "outputs": [], "source": [ "def parse_email(email):\n", " result = re.match(\n", " r\"(?P[\\.\\w\\-\\!~#$%&\\|{}\\+\\/\\^\\`\\=\\*']+).(?P[\\w\\.\\-]+)\",\n", " email,\n", " ).groups()\n", " return result" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "execution": { "iopub.execute_input": "2026-05-22T13:50:35.733637Z", "iopub.status.busy": "2026-05-22T13:50:35.733065Z", "iopub.status.idle": "2026-05-22T13:50:36.074238Z", "shell.execute_reply": "2026-05-22T13:50:36.073874Z", "shell.execute_reply.started": "2026-05-22T13:50:35.733594Z" } }, "outputs": [], "source": [ "test_parse_email()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3.13 Kernel", "language": "python", "name": "python313" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }