.. _unittesting: Unit 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. .. 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. Dort hinein kommen unsere Unit-Tests. Dieses Verzeichnis muss ein Python-Package sein, deshalb kommt auch noch eine ``__init__.py`` hinein. 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 .. 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 blog 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 Views testen =============== 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 wurden. 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_views_1.py Testabdeckung prüfen ======================= .. code-block:: bash pip install coverage pip install django-nose in den ``settings/dev.py`` ............................. .. code-block:: python INSTALLED_APPS = ( # ... 'django_nose', ) # Use nose to run all tests. May not work on Windows # on Windows: coverage run --source=events ./manage.py test # and comment out TEST_RUNNER TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' # Tell nose to measure coverage on the 'events' and 'user' apps NOSE_ARGS = [ '--with-coverage', '--cover-package=events,user', ] .coveragerc ............. Alles mit tests im Namen wird von der Coverage ignoriert, also die ganzen Testklassen- und Methoden bzw. Migrationen. .. code-block:: bash [run] omit = *tests*, *migrations* in .gitignore: ................ Alle Files, die mit dem Coverage zusammenhängen, werden in der Versionskontrolle ignoriert. Die ``.coveragerc`` wird nicht ignoriert. .. code-block:: bash htmlcov/ .coverage .coverage.* coverage.xml *.cover Testdurchlauf starten -------------------------- .. code-block:: bash python manage.py test # oder verbose ... python manage.py test events -v 2 Falls in Windows ein Fehler auftreten sollte, kann man den Test auch so starten: .. code-block:: bash coverage run --source=events,user ./manage.py test einfachen Report printen: -------------------------------- .. code-block:: bash coverage report coverage html ---------------- Nach dem Testlauf ``coverage html`` eingeben. Das verzeichnis ``htmlcov`` wird erstellt. Öffne ``htmlcov/index.html`` mit dem Browser. .. code-block:: bash coverage html Weiterführende Infos: -------------------------------- coverage mit nose: https://django-testing-docs.readthedocs.io/en/latest/coverage.html über statement und branch covering https://lautaportti.wordpress.com/2011/05/07/test-coverage-analysis/ 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. Statement Coverage ---------------------- Einfachste Coverage Art. Prüft, ob jede Zeile des Code in einem Test gelaufen sind. Damit sind oft Coverages von 100% möglich. number_of_code_lines_run_at_least_once_under_tests / total_number_of lines) * 100% Branch Coverage ---------------------- If-Blöcke, sogenannte Branches, werden auch geprüft. er 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. Selbst bei einer 100-prozentigen Anweisungsabdeckung ist es ziemlich einfach, weniger als 100 % Filialabdeckung zu haben. number_of_branches_executed_at_least_once_under_tests / all_branches) * 100% .coveragerc anlegen .. code-block:: bash [run] branch = True muss dann so gestartet werden: coverage run --branch --source=events ./manage.py test Weiterführende Infos: ------------------------ Templates mit dem Plugin für ``coverage`` testen: https://pypi.org/project/django-coverage-plugin/