.. _unittesting: .. index:: single: Testen der Endpunkte single: Unit-Testing single: Test-Client single: Test-Abdeckung single: Coverage Testing mit Django ************************ Web-Anwendungen sind komplex und sollten einem permanenten Testing-Verfahren unterliegen. Django kommt mit einem mitgeliefertem Testing-Framework, was für diese Aufgabe unkompliziert nutzen können. Mit dem Testing-Framework können wir neben sogenannten Unit-Tests auch einfache Integrationstests schreiben. .. admonition:: Was ist Unit-Testing? Um die Effizienz von Tests zu verbessern, ist es üblich, Tests in Einheiten zu unterteilen, die bestimmte Funktionen der Webanwendung testen. Diese Praxis wird als Unit-Test bezeichnet. Es erleichtert das Erkennen von Fehlern, da sich die Tests unabhängig von anderen Teilen auf kleine Teile (Units) Ihres Projekts konzentrieren. Unit-Tests in Django sind nichts weiter als Python-Module, die wir mit dem Namen "test" präfixen und die Methoden enthalten, die ebenfalls den Präfix "test" haben. Ein solches Modul könnten zum Beispiel ``test_forms.py`` heissen und Klassen implementieren, die Formulare testen. Wir wollen nun für die App ``events`` folgende Dinge testen: * Models testen * Views testen Models testen =============== Wir löschen die Datei ``event_manager/events/tests.py`` und legen das Verzeichnis ``event_manager/events/tests`` an. Der Grund ist, dass in einem Projekt oft viele Dinge getestet werden, und es übersichtlicher ist, für jede Art von Test ein eigenes Verzeichnis anzulegen, statt alles in eine ``tests.py``-Datei zu schreiben. Dort hinein kommen unsere Unit-Tests. Dieses Verzeichnis muss ein Python-Package sein, deshalb legen wir hier auch noch eine ``__init__.py`` an. Datei anlegen --------------- Wir legen die Datei ``event_manager/events/tests/test_models.py`` an und füllen sie mit folgendem Inhalt: .. literalinclude:: ../../../src/events/test_model_1.py Aus ``django.test`` importieren wir ``TestCase``. Von dieser Klasse erben unsere ganzen Testklassen. Dann legen wir die Testklasse ``EventModelTests`` an, und definieren eine ``setUp``-Methode, die **VOR** jeder Test-Methode aufgerufen wird. Dort erstellen wir auf Basis unserer ``Factories`` einen User und eine Categorie und erstellen das Dict, mit dem wir in den Methoden dann Event-Objekt bauen. In der Methode ``self.event_payload`` prüfen wir zum Beispiel, ob der Slug, der von Django erstellt wurde, unseren Erwartungen entspricht. In der Methode ``test_invalid_event_name_too_short`` prüfen wir, ob bei der Eingabe eines zu kurzen Eventnames ein Validation-Error ausgelöst wird. Wir können den Test jetzt starten. .. code-block:: bash python manage.py test Wir bekommen folgende Meldung serviert: .. code-block:: bash File "event_manager\events\tests\test_models.py", line 67, in teste test_invalid_event_name_too_short self.assertRaises(ValidationError, event.full_clean) AssertionError: ValidationError not raised by full_clean Der Grund ist natürlich, dass wir bisher noch keinen Validation-Error auslösen, wenn wir einen zu kurzen Namen eintragen. Das holen wir jetzt nach und öffnen die Datei ``event_manager/events/models.py`` und importieren den ``MinLengthValidator``. .. code-block:: python from django.core.validators import MinLengthValidator Dann ändern wir das Event-Model ab: .. code-block:: python class Event(DateMixin): """Stores a single event entry. related to :model:`events.Category` and :model:`user.User`.""" # anderer Code name = models.CharField( max_length=100, unique=True, validators=[MinLengthValidator(3)] ) der ``MinLengthValidator`` löst einen Validierungsfehler aus, wenn ein Wert kleiner als 3 eingegeben wird. Wir fügen noch die Methode ``test_event_creation_with_valid_values`` hinzu, die prüft, ob ein Objekt mit richtigen Werten auch sauber angelegt wurde. .. literalinclude:: ../../../src/events/test_model_2.py Integration Test der Views ============================ Django bietet eine Möglichkeit, Views zu testen, wenn gleich auch nicht so umfangreich, wie das zb. ``Selenium`` macht. Trotzdem können wir mit einem ``Client`` prüfen, ob zum Beispiel Formulare erfolgreich abgeschickt und die Daten erfolgreich eintragen wurden. Datei anlegen --------------- Wir legen die Datei ``event_manager/events/tests/test_views.py`` an und füllen sie mit folgendem Inhalt: .. literalinclude:: ../../../src/events/test_views_1.py Testabdeckung prüfen ======================= Um die Testabdeckung zu testen, dh. um zu prüfen, ob alle Klassen und Funktionen im Test geprüft wurden, nutzen wir das Paket ``coverage``. wir fügen das ``coverage``-Paket der ``event_manager/requirements-dev.in`` hinzu und installieren es. .. code-block:: bash ... coverage .. code-block:: bash (eventenv) pip-compile requirements.in requirements-dev.in (eventenv) pip-sync requirements.txt requirements-dev.txt .coveragerc --------------- Tests- und Migrationsdateien wollen wir ignorieren. Dazu legen wir unter ``event_manager/.coveragerc`` die Configurationsdatei für ``coverage`` an und kopieren folgende Settings dort hinein: .. code-block:: bash [run] source = . omit = ./env/* */tests/* *apps.py manage.py *__init__.py */migrations/* *asgi* *wsgi* *urls.py [report] omit = ./env/* */tests/* *apps.py manage.py *__init__.py */migrations/* *asgi* *wsgi* *urls.py show_missing = True [html] directory = tests/coverage/html_report/ in .gitignore: --------------- Alle Files, die mit dem Coverage zusammenhängen, werden in der Versionskontrolle ignoriert. Die ``.coveragerc`` wird allerdings nicht ignoriert, da hier die Einstellungen eingetragen werden. Dazu folgende Zeilen in die ``.gitignore`` eintragen. .. code-block:: bash htmlcov/ html_report/ .coverage .coverage.* coverage.xml *.cover Testdurchlauf mit Coverage starten ----------------------------------- .. code-block:: bash coverage erase coverage run manage.py test # oder verbose ... coverage run manage.py test -v 2 Coverage hat nun eine ``.coverage`` Datei angelegt, in der sich die Ergebnisse des Testlaufs finden. Diese Datei ist allerdings eine Binärdatei, mit der sich so erstmal nicht viel anfangen lässt. einfachen Report printen: -------------------------------- Um einen einfachen Report auf der Shell auszudrucken, genügt dieser Befehl: .. code-block:: bash coverage report coverage html ---------------- Um eine etwas komfortablere Ausgabe zu haben, kann man sich die Daten auch als HTML-Report erstellen lassen. Dazu statt ``coverage report`` einfach ``coverage html`` eingeben. Das verzeichnis ``tests/coverage/html_report/`` wird erstellt. Um den Inhalt zu betrachen, starten wir in diesem Verzeichnis den Python-Webserver und rufen dann mit dem Browser die Adresse ``http://127.0.0.1:8080`` auf: .. code-block:: bash coverage html cd tests/coverage/html_report python -m http.server 8080 Coverage Daten löschen -------------------------- Um alle Daten, die von coverage angelegt wurden zu löschen, kann man diesen Befehl nutzen: .. code-block:: bash coverage erase Die Coverage Arten ==================== Vereinfacht ausgedrückt ist die Codeabdeckung ein Maß für die Vollständigkeit einer Testsuite. 100 % Codeabdeckung bedeutet, dass ein System vollständig getestet ist. Die Testabdeckung sagt allerdings nichts über die Qualität der Tests aus. Statement Coverage ---------------------- Einfachste Coverage Art. Prüft, ob jede Zeile des Code in einem Test mindestens einmal gelaufen ist. Damit sind oft Coverages von 100% möglich. **Das Statement Coverate berechnet sich in Prozent wie folgt:** .. code-block:: python (nr_of_code_lines_run_at_least_once / total_number_of lines) * 100% Branch Coverage ---------------------- If-Blöcke, sogenannte Branches, werden im Branch Coverage auch geprüft. Der Zweck der ``Verzweigungsabdeckungsanalyse`` besteht darin, die logischen Verzweigungen bei der Ausführung des Codes zu verfolgen und anzuzeigen, ob einige logische Pfade während des Testlaufs nicht ausgeführt werden. **Das Branch Coverage berechnet sich in Prozent wie folgt:** .. code-block:: python (nr_of_branches_executed_at_least_once / total_number_of lines) * 100% Branch Covering in der ``.coveragerc`` festlegen. .. code-block:: bash [run] branch = True Dann wie gewohnt den Test via coverage starten Weiterführende Infos: ------------------------ `Coverage Doku `_ `Django templates Coverage `_