.. _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 `_