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.
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:
from django.core.exceptions import ValidationError
from django.test import Client, TestCase
from django.utils import timezone
from events.models import Event
from events.factories import CategoryFactory
from core.factories import UserFactory
def create_user():
return UserFactory()
class EventModelTests(TestCase):
def setUp(self):
"""This method runs before each test in a test class."""
cat = CategoryFactory()
author = create_user()
self.event_payload = {
"name": "Seilspringen und Boxen",
"description": "Spaß muss sein",
"sub_title": "Seilspringen für Anfänger",
"category": cat,
"min_group": 10,
"date": timezone.now(),
"author": author,
}
def test_if_event_has_proper_slug(self):
event = Event.objects.create(**self.event_payload)
event.full_clean()
self.assertEqual(event.slug, "seilspringen-und-boxen")
def test_invalid_event_name_too_short(self):
"""event name must be greater than 2 chars"""
self.event_payload["name"] = "aa"
event = Event.objects.create(**self.event_payload)
self.assertRaises(ValidationError, event.full_clean)
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.
python manage.py test
Wir bekommen folgende Meldung serviert:
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
.
from django.core.validators import MinLengthValidator
Dann ändern wir das Event-Model ab:
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.
from django.core.exceptions import ValidationError
from django.test import Client, TestCase
from django.utils import timezone
from events.models import Event
from events.factories import CategoryFactory
from core.factories import UserFactory
def create_user():
return UserFactory()
class EventModelTests(TestCase):
def setUp(self):
"""This method runs before each test in a test class."""
self.cat = CategoryFactory()
self.author = create_user()
self.event_payload = {
"name": "Seilspringen und Boxen",
"description": "Spaß muss sein",
"sub_title": "Seilspringen für Anfänger",
"category": self.cat,
"min_group": 10,
"date": timezone.now(),
"author": self.author,
}
def test_if_event_has_proper_slug(self):
event = Event.objects.create(**self.event_payload)
event.full_clean()
self.assertEqual(event.slug, "seilspringen-und-boxen")
def test_event_creation_with_valid_values(self):
"""test creating review with an invalid rating"""
event = Event(**self.event_payload)
event.save()
event.full_clean()
event = Event.objects.all()
self.assertTrue(event.exists())
self.assertEqual(event[0].name, "Seilspringen und Boxen")
self.assertEqual(event[0].slug, "seilspringen-und-boxen")
self.assertEqual(event[0].sub_title, "Seilspringen für Anfänger")
self.assertEqual(event[0].min_group, 10)
self.assertEqual(event[0].category.id, self.cat.id)
self.assertEqual(event[0].author, self.author)
self.assertEqual(len(event), 1)
def test_invalid_event_name_too_short(self):
"""event name must be greater than 2 chars"""
self.event_payload["name"] = "aa"
event = Event.objects.create(**self.event_payload)
self.assertRaises(ValidationError, event.full_clean)
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:
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import timezone
from events.factories import CategoryFactory
from core.factories import UserFactory
from events.models import Event
def create_user():
return UserFactory()
class EventFormTests(TestCase):
def setUp(self):
author = create_user()
self.client = Client()
self.client.force_login(author)
self.cat = CategoryFactory()
self.payload = {
"name": "Seilspringen und Boxen",
"description": "Spaß muss sein",
"sub_title": "Seilspringen für Anfänger",
"min_group": 10,
"date": timezone.now(),
}
def test_if_event_is_correct_saved(self):
res = self.client.post(
reverse("events:create_event2", args=(self.cat.id,)),
self.payload,
)
# wird weitergeleitet nach Anlegen?
self.assertEquals(res.status_code, 302)
# existiert Objekt in Datenbank?
event = Event.objects.get(name=self.payload["name"])
# ist der slug richtig?
self.assertEqual(event.slug, "seilspringen-und-boxen")
def test_invalid_min_group_number(self):
"""Test if object is created via form with a invalid min group
only 5, 10 and 20 is allowed. No Redirection, Status 200
"""
self.payload["min_group"] = 7
res = self.client.post(
reverse("events:create_event2", args=(self.cat.id,)),
self.payload,
)
self.assertEquals(res.status_code, 200)
events = Event.objects.all() # lfilter(name=self.payload["name"])
self.assertFalse(events.exists())
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.
...
coverage
(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:
[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.
htmlcov/
html_report/
.coverage
.coverage.*
coverage.xml
*.cover
Testdurchlauf mit Coverage starten
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:
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:
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:
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:
(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:
(nr_of_branches_executed_at_least_once / total_number_of lines) * 100%
Branch Covering in der .coveragerc
festlegen.
[run]
branch = True
Dann wie gewohnt den Test via coverage starten