Django-Tutorial Teil 10: Testen einer Django-Webanwendung

Wenn Websites wachsen, werden sie schwieriger manuell zu testen. Nicht nur, dass mehr getestet werden muss, sondern auch, weil die Interaktionen zwischen Komponenten komplexer werden, kann eine kleine Änderung in einem Bereich andere Bereiche beeinflussen, sodass mehr Änderungen erforderlich sind, um sicherzustellen, dass alles weiterhin funktioniert und keine Fehler eingeführt werden, wenn mehr Änderungen vorgenommen werden. Eine Möglichkeit, diese Probleme zu mildern, besteht darin, automatisierte Tests zu schreiben, die einfach und zuverlässig jedes Mal ausgeführt werden können, wenn Sie eine Änderung vornehmen. Dieses Tutorial zeigt, wie Sie das Unit Testing Ihrer Website mithilfe des Test-Frameworks von Django automatisieren können.

Voraussetzungen: Absolvieren Sie alle vorherigen Tutorial-Themen, einschließlich Django-Tutorial Teil 9: Arbeiten mit Formularen.
Ziel: Verständnis, wie Unit-Tests für Django-basierte Websites geschrieben werden.

Überblick

Die Lokale Bibliothek hat derzeit Seiten, um Listen aller Bücher und Autoren anzuzeigen, Detailansichten für Book- und Author-Einträge, eine Seite zum Erneuern von BookInstance-Einträgen und Seiten zum Erstellen, Aktualisieren und Löschen von Author-Einträgen (und auch Book-Datensätzen, wenn Sie die Herausforderung im Formular-Tutorial abgeschlossen haben). Selbst bei dieser relativ kleinen Seite kann das manuelle Navigieren zu jeder Seite und das oberflächliche Überprüfen, ob alles wie erwartet funktioniert, mehrere Minuten dauern. Wenn wir Änderungen vornehmen und die Seite vergrößern, wird die Zeit, die erforderlich ist, um manuell zu überprüfen, ob alles „richtig“ funktioniert, nur noch zunehmen. Wenn wir so weitermachen wie bisher, würden wir irgendwann die meiste Zeit mit Testen verbringen und nur sehr wenig Zeit darauf verwenden, unseren Code zu verbessern.

Automatisierte Tests können bei diesem Problem wirklich helfen! Die offensichtlichen Vorteile sind, dass sie viel schneller als manuelle Tests durchgeführt werden können, sie testen mit einem viel geringeren Detailgrad und testen jedes Mal genau die gleiche Funktionalität (menschliche Tester sind bei weitem nicht so zuverlässig!) Da sie schnell sind, können automatisierte Tests regelmäßig ausgeführt werden, und falls ein Test fehlschlägt, zeigt er genau, wo der Code nicht wie erwartet funktioniert.

Darüber hinaus können automatisierte Tests als erster echter „Benutzer“ Ihres Codes fungieren, der Sie dazu zwingt, rigoros zu definieren und zu dokumentieren, wie sich Ihre Website verhalten soll. Häufig sind sie die Grundlage für Ihre Code-Beispiele und Dokumentationen. Aus diesen Gründen beginnen einige Software-Entwicklungsprozesse mit der Testdefinition und Implementierung, wonach der Code geschrieben wird, um das erforderliche Verhalten widerzuspiegeln (z. B. testgetrieben und verhaltensgetrieben Entwicklung).

Dieses Tutorial zeigt, wie Sie automatisierte Tests für Django schreiben, indem Sie der LocalLibrary-Website eine Reihe von Tests hinzufügen.

Arten des Testens

Es gibt zahlreiche Arten, Ebenen und Klassifikationen von Tests und Testansätzen. Die wichtigsten automatisierten Tests sind:

Unit-Tests

Überprüfen Sie das funktionale Verhalten einzelner Komponenten, oft auf Klassen- und Funktionsebene.

Regressionstests

Tests, die historische Fehler reproduzieren. Jeder Test wird zunächst durchgeführt, um zu überprüfen, ob der Fehler behoben wurde, und dann erneut ausgeführt, um sicherzustellen, dass er nach späteren Änderungen am Code nicht wieder eingeführt wurde.

Integrationstests

Überprüfen Sie, wie Gruppen von Komponenten zusammenarbeiten, wenn sie gemeinsam verwendet werden. Integrationstests berücksichtigen die erforderlichen Interaktionen zwischen Komponenten, jedoch nicht unbedingt die internen Operationen jeder Komponente. Sie können einfache Gruppierungen von Komponenten bis hin zur gesamten Website abdecken.

Hinweis: Andere häufige Arten von Tests sind Black-Box-, White-Box-, manuelle, automatisierte, Kanarienvogel-, Rauch-, Konformitäts-, Akzeptanz-, funktionale, System-, Performance-, Last- und Stresstests. Suchen Sie danach, um mehr Informationen zu erhalten.

Was bietet Django zum Testen?

Das Testen einer Website ist eine komplexe Aufgabe, da sie aus mehreren Logikebenen besteht – von der HTTP-Anfrageverarbeitungsebene über Modellabfragen bis hin zur Formularvalidierung und -verarbeitung sowie der Vorlagenwiedergabe.

Django bietet ein Test-Framework mit einer kleinen Hierarchie von Klassen, die auf der Python-Standardbibliothek unittest aufbauen. Trotz des Namens eignet sich dieses Test-Framework sowohl für Unit- als auch für Integrationstests. Das Django-Framework fügt API-Methoden und -Tools hinzu, die helfen, das Web- und Django-spezifische Verhalten zu testen. Diese ermöglichen es Ihnen, Anfragen zu simulieren, Testdaten einzufügen und die Ausgabe Ihrer Anwendung zu inspizieren. Django bietet auch eine API (LiveServerTestCase) und Werkzeuge zur Verwendung verschiedener Test-Frameworks. So können Sie z. B. das beliebte Selenium-Framework integrieren, um einen Benutzer zu simulieren, der mit einem Live-Browser interagiert.

Um einen Test zu schreiben, leiten Sie von einer der Django- (oder unittest) Test-Basisklassen (SimpleTestCase, TransactionTestCase, TestCase, LiveServerTestCase) ab und schreiben dann separate Methoden, um zu überprüfen, dass bestimmte Funktionalitäten wie erwartet funktionieren (Tests verwenden „assert“-Methoden, um zu testen, dass Ausdrücke in True oder False-Werten resultieren oder dass zwei Werte gleich sind, etc.). Wenn Sie einen Testrun starten, führt das Framework die ausgewählten Testmethoden in Ihren abgeleiteten Klassen aus. Die Testmethoden werden unabhängig ausgeführt, mit allgemeinem Setup und/oder Tear-Down-Verhalten, das in der Klasse definiert ist, wie unten gezeigt.

python
class YourTestClass(TestCase):
    def setUp(self):
        # Setup run before every test method.
        pass

    def tearDown(self):
        # Clean up run after every test method.
        pass

    def test_something_that_will_pass(self):
        self.assertFalse(False)

    def test_something_that_will_fail(self):
        self.assertTrue(False)

Die beste Basisklasse für die meisten Tests ist django.test.TestCase. Diese Testklasse erstellt eine saubere Datenbank, bevor ihre Tests ausgeführt werden, und führt jede Testfunktion in ihrer eigenen Transaktion aus. Die Klasse besitzt auch einen Test-Client, den Sie verwenden können, um einen Benutzer zu simulieren, der auf der Ansichtsebene mit dem Code interagiert. In den folgenden Abschnitten konzentrieren wir uns auf Unit-Tests, die mit dieser TestCase-Basisklasse erstellt wurden.

Hinweis: Die django.test.TestCase-Klasse ist sehr bequem, kann jedoch dazu führen, dass einige Tests langsamer sind, als sie sein müssten (nicht jeder Test muss seine eigene Datenbank einrichten oder die Ansichtssimulation durchführen). Sobald Sie vertraut sind mit dem, was Sie mit dieser Klasse tun können, möchten Sie möglicherweise einige Ihrer Tests durch die verfügbaren einfacheren Testklassen ersetzen.

Was sollten Sie testen?

Sie sollten alle Aspekte Ihres eigenen Codes testen, jedoch nicht Bibliotheken oder Funktionen, die als Teil von Python oder Django bereitgestellt werden.

Betrachten Sie zum Beispiel das unten definierte Author-Modell. Sie müssen nicht explizit testen, dass first_name und last_name korrekt als CharField in der Datenbank gespeichert wurden, weil das etwas ist, das von Django definiert wird (obwohl Sie in der Praxis diese Funktionalität während der Entwicklung zwangsläufig testen werden). Ebenso müssen Sie nicht testen, dass das date_of_birth als Datumsfeld validiert wurde, weil das wiederum etwas ist, das in Django implementiert ist.

Sie sollten jedoch den Text für die Labels (First name, Last name, Date of birth, Died) und die Feldgröße für den Text (100 Zeichen) überprüfen, da diese Teile Ihres Designs sind und in Zukunft gebrochen/geändert werden könnten.

python
class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])

    def __str__(self):
        return '%s, %s' % (self.last_name, self.first_name)

Ebenso sollten Sie überprüfen, ob die benutzerdefinierten Methoden get_absolute_url() und __str__() wie erforderlich funktionieren, da sie ihr Code/Geschäftslogik sind. Im Fall von get_absolute_url() können Sie darauf vertrauen, dass die Django-Methode reverse() korrekt implementiert wurde, sodass Sie testen, ob die zugehörige Ansicht tatsächlich definiert wurde.

Hinweis: Aufmerksame Leser könnten anmerken, dass wir auch das Geburts- und Todesdatum auf sinnvolle Werte beschränken und überprüfen möchten, dass der Tod nach der Geburt liegt. In Django würde diese Einschränkung in Ihre Formklassen eingebunden werden (obwohl Sie Validierer für Modellfelder und Modellvalidierer definieren können, werden diese nur auf der Formularebene verwendet, wenn sie von der clean()-Methode des Modells aufgerufen werden. Dies erfordert ein ModelForm, oder die clean()-Methode des Modells muss speziell aufgerufen werden.)

Mit diesem Hintergrund beginnen wir zu schauen, wie man Tests definiert und durchführt.

Überblick über die Teststruktur

Bevor wir ins Detail gehen, was getestet werden soll, lassen Sie uns zunächst kurz schauen, wo und wie Tests definiert sind.

Django verwendet die eingebaute Testfindung des unittest-Moduls, die Tests unter dem aktuellen Arbeitsverzeichnis in jeder Datei mit dem Muster test*.py findet. Vorausgesetzt, Sie benennen die Dateien entsprechend, können Sie jede beliebige Struktur verwenden. Wir empfehlen, ein Modul für Ihren Testcode zu erstellen und separate Dateien für Modelle, Ansichten, Formulare und andere zu testende Codearten zu verwenden. Zum Beispiel:

catalog/
  /tests/
    __init__.py
    test_models.py
    test_forms.py
    test_views.py

Erstellen Sie eine Dateistruktur wie oben in Ihrem Projekt LocalLibrary. Die __init__.py sollte eine leere Datei sein (dies zeigt Python, dass das Verzeichnis ein Paket ist). Sie können die drei Testdateien erstellen, indem Sie die Skelettdatei /catalog/tests.py kopieren und umbenennen.

Hinweis: Die Skelett-Testdatei /catalog/tests.py wurde automatisch erstellt, als wir die Django-Skelett-Website erstellt haben. Es ist völlig „legal“, alle Ihre Tests darin zu speichern, aber wenn Sie richtig testen, werden Sie schnell eine sehr große und unübersichtliche Testdatei haben.

Löschen Sie die Skelettdatei, da wir sie nicht benötigen werden.

Öffnen Sie /catalog/tests/test_models.py. Die Datei sollte django.test.TestCase importieren, wie unten gezeigt:

python
from django.test import TestCase

# Create your tests here.

Oft fügen Sie für jedes Modell/View/Formular, das Sie testen möchten, eine Testklasse mit individuellen Methoden hinzu, um spezifische Funktionalitäten zu testen. In anderen Fällen möchten Sie möglicherweise eine separate Klasse haben, um einen bestimmten Anwendungsfall zu testen, mit individuellen Testfunktionen, die Aspekte dieses Anwendungsfalls testen (zum Beispiel eine Klasse, die überprüft, ob ein Modellfeld ordnungsgemäß validiert wurde, mit Funktionen zum Testen jedes möglichen Fehlerszenarios). Wiederum ist die Struktur sehr Ihnen überlassen, aber es ist am besten, wenn Sie konsistent sind.

Fügen Sie die Testklasse unten am Ende der Datei hinzu. Die Klasse demonstriert, wie man eine Testfallklasse konstruiert, indem man von TestCase ableitet.

python
class YourTestClass(TestCase):
    @classmethod
    def setUpTestData(cls):
        print("setUpTestData: Run once to set up non-modified data for all class methods.")
        pass

    def setUp(self):
        print("setUp: Run once for every test method to set up clean data.")
        pass

    def test_false_is_false(self):
        print("Method: test_false_is_false.")
        self.assertFalse(False)

    def test_false_is_true(self):
        print("Method: test_false_is_true.")
        self.assertTrue(False)

    def test_one_plus_one_equals_two(self):
        print("Method: test_one_plus_one_equals_two.")
        self.assertEqual(1 + 1, 2)

Die neue Klasse definiert zwei Methoden, die Sie für die Vorkonfiguration von Tests verwenden können (zum Beispiel, um Modelle oder andere Objekte zu erstellen, die Sie für den Test benötigen):

  • setUpTestData() wird einmal zu Beginn des Testruns für das Setup auf Klassenebene aufgerufen. Sie würden dies verwenden, um Objekte zu erstellen, die in keiner der Testmethoden modifiziert oder geändert werden.
  • setUp() wird vor jeder Testfunktion aufgerufen, um alle Objekte einzurichten, die möglicherweise durch den Test geändert werden (jede Testfunktion erhält eine „neue“ Version dieser Objekte).

Hinweis: Die Testklassen haben auch eine tearDown()-Methode, die wir nicht verwendet haben. Diese Methode ist für Datenbanktests nicht besonders nützlich, da die TestCase-Basisklasse die Datenbank selbst abbaut.

Unterhalb dieser haben wir eine Reihe von Testmethoden, die Assert-Funktionen verwenden, um zu überprüfen, ob Bedingungen wahr, falsch oder gleich sind (AssertTrue, AssertFalse, AssertEqual). Wenn die Bedingung nicht wie erwartet ausgewertet wird, schlägt der Test fehl und meldet den Fehler in Ihrer Konsole.

Die AssertTrue, AssertFalse, AssertEqual sind Standardassertionen, die von unittest bereitgestellt werden. Es gibt andere Standardassertionen im Framework und auch Django-spezifische Assertionen, um zu testen, ob eine Ansicht umleitet (assertRedirects), um zu testen, ob eine bestimmte Vorlage verwendet wurde (assertTemplateUsed), usw.

Hinweis: Sie sollten normalerweise keine print()-Funktionen in Ihren Tests enthalten, wie oben gezeigt. Wir tun dies hier nur, damit Sie die Reihenfolge sehen können, in der die Setup-Funktionen in der Konsole aufgerufen werden (im folgenden Abschnitt).

Wie man die Tests ausführt

Der einfachste Weg, alle Tests auszuführen, besteht darin, den Befehl zu verwenden:

bash
python3 manage.py test

Dies wird alle Dateien mit dem Muster test*.py unter dem aktuellen Verzeichnis entdecken und alle Tests ausführen, die mit geeigneten Basisklassen definiert sind (hier haben wir eine Reihe von Testdateien, aber nur /catalog/tests/test_models.py enthält derzeit Tests). Standardmäßig berichten die Tests nur einzeln über Testfehler, gefolgt von einer Testzusammenfassung.

Hinweis: Wenn Sie Fehler ähnlich wie ValueError: Missing staticfiles manifest entry... erhalten, könnte dies daran liegen, dass das Testen collectstatic standardmäßig nicht ausführt und Ihre App eine Speicherklasse verwendet, die dies erfordert (siehe manifest_strict für weitere Informationen). Es gibt eine Reihe von Möglichkeiten, dieses Problem zu überwinden – die einfachste ist, collectstatic auszuführen, bevor Sie die Tests ausführen:

bash
python3 manage.py collectstatic

Führen Sie die Tests im Stammverzeichnis der LocalLibrary aus. Sie sollten eine Ausgabe wie die unten stehende sehen.

bash
> python3 manage.py test

Creating test database for alias 'default'...
setUpTestData: Run once to set up non-modified data for all class methods.
setUp: Run once for every test method to set up clean data.
Method: test_false_is_false.
setUp: Run once for every test method to set up clean data.
Method: test_false_is_true.
setUp: Run once for every test method to set up clean data.
Method: test_one_plus_one_equals_two.
.
======================================================================
FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\GitHub\django_tmp\library_w_t_2\locallibrary\catalog\tests\tests_models.py", line 22, in test_false_is_true
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 3 tests in 0.075s

FAILED (failures=1)
Destroying test database for alias 'default'...

Hier sehen wir, dass wir einen Testfehler hatten und wir können genau sehen, welche Funktion fehlgeschlagen ist und warum (dieser Fehler ist erwartet, weil False nicht True ist!).

Hinweis: Das wichtigste, was Sie aus der Testausgabe oben lernen sollten, ist, dass es viel wertvoller ist, wenn Sie beschreibende/informative Namen für Ihre Objekte und Methoden verwenden.

Die Ausgabe der Funktionen print() zeigt, wie die Methode setUpTestData() einmal für die Klasse und setUp() vor jeder Methode aufgerufen wird. Erneut sei daran erinnert, dass Sie normalerweise solche print()-Befehle nicht in Ihre Tests einfügen würden.

Die nächsten Abschnitte zeigen, wie Sie spezifische Tests ausführen und wie Sie steuern können, wie viele Informationen die Tests anzeigen.

Mehr Testinformationen anzeigen

Wenn Sie mehr Informationen über den Testrun erhalten möchten, können Sie die Verbosity ändern. Zum Beispiel, um die Erfolge ebenso wie die Misserfolge aufzulisten (und eine Menge Informationen darüber, wie die Testdatenbank eingerichtet wird), können Sie die Verbosity auf "2" setzen, wie gezeigt:

bash
python3 manage.py test --verbosity 2

Die erlaubten Verbosity-Stufen sind 0, 1, 2 und 3, wobei die Standardeinstellung „1“ ist.

Beschleunigung

Wenn Ihre Tests unabhängig sind, können Sie diese auf einem Multiprozessor-Rechner erheblich beschleunigen, indem Sie sie parallel ausführen. Die Verwendung von --parallel auto unten führt einen Testprozess pro verfügbarem Kern aus. Das auto ist optional, und Sie können auch eine bestimmte Anzahl von Kernen angeben, die verwendet werden sollen.

bash
python3 manage.py test --parallel auto

Für weitere Informationen, einschließlich was zu tun ist, wenn Ihre Tests nicht unabhängig sind, siehe DJANGO_TEST_PROCESSES.

Spezifische Tests ausführen

Wenn Sie einen Teil Ihrer Tests ausführen möchten, können Sie dies tun, indem Sie den vollständigen Punktpfad zu den Paketen, dem Modul, der TestCase-Unterklasse oder der Methode angeben:

bash
# Run the specified module
python3 manage.py test catalog.tests

# Run the specified module
python3 manage.py test catalog.tests.test_models

# Run the specified class
python3 manage.py test catalog.tests.test_models.YourTestClass

# Run the specified method
python3 manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two

Andere Optionen des Test-Läufers

Der Test-Läufer bietet viele andere Optionen, einschließlich der Möglichkeit, Tests zu mischen (--shuffle), sie im Debug-Modus auszuführen (--debug-mode) und den Python-Logger zu verwenden, um die Ergebnisse aufzufangen. Weitere Informationen finden Sie in der Django-Dokumentation des Test-Läufers.

LocalLibrary-Tests

Jetzt wissen wir, wie man unsere Tests ausführt und was wir testen müssen, lassen Sie uns einige praktische Beispiele betrachten.

Hinweis: Wir werden nicht jeden möglichen Test schreiben, aber dies sollte Ihnen eine Vorstellung davon geben, wie Tests funktionieren und was Sie noch tun können.

Modelle

Wie oben besprochen, sollten Sie alles testen, was Teil Ihres Designs ist oder von Code, den wir selbst geschrieben haben, definiert wird, aber nicht das Verhalten des zu Grunde liegenden Frameworks und anderer Drittanbieter-Bibliotheken.

Betrachten Sie zum Beispiel das Author-Modell unten. Hier sollten wir die Bezeichnungen für alle Felder testen, weil selbst wenn wir die meisten von ihnen nicht explizit angegeben haben, wir ein Design haben, das besagt, was diese Werte sein sollten. Wenn wir die Werte nicht testen, dann wissen wir nicht, dass die Feldbezeichnungen ihre beabsichtigten Werte haben. Ebenso vertrauen wir zwar darauf, dass Django ein Feld der angegebenen Länge erstellt, es ist jedoch sinnvoll, einen Test für diese Länge anzugeben, um sicherzustellen, dass es wie geplant implementiert wurde.

python
class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])

    def __str__(self):
        return f'{self.last_name}, {self.first_name}'

Öffnen Sie unser /catalog/tests/test_models.py und ersetzen Sie vorhandenen Code durch den folgenden Testcode für das Autor-Modell.

Hier werden wir TestCase importieren und unsere Testklasse (AuthorModelTest) davon ableiten, einen beschreibenden Namen verwenden, damit wir fehlschlagende Tests in der Ausgabe leicht identifizieren können. Dann rufen wir setUpTestData() auf, um ein Autor-Objekt zu erstellen, das wir verwenden, aber in keinem der Tests ändern werden.

python
from django.test import TestCase

from catalog.models import Author

class AuthorModelTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up non-modified objects used by all test methods
        Author.objects.create(first_name='Big', last_name='Bob')

    def test_first_name_label(self):
        author = Author.objects.get(id=1)
        field_label = author._meta.get_field('first_name').verbose_name
        self.assertEqual(field_label, 'first name')

    def test_date_of_death_label(self):
        author = Author.objects.get(id=1)
        field_label = author._meta.get_field('date_of_death').verbose_name
        self.assertEqual(field_label, 'died')

    def test_first_name_max_length(self):
        author = Author.objects.get(id=1)
        max_length = author._meta.get_field('first_name').max_length
        self.assertEqual(max_length, 100)

    def test_object_name_is_last_name_comma_first_name(self):
        author = Author.objects.get(id=1)
        expected_object_name = f'{author.last_name}, {author.first_name}'
        self.assertEqual(str(author), expected_object_name)

    def test_get_absolute_url(self):
        author = Author.objects.get(id=1)
        # This will also fail if the urlconf is not defined.
        self.assertEqual(author.get_absolute_url(), '/catalog/author/1')

Die Feldtests überprüfen, ob die Werte der Feldbezeichnungen (verbose_name) und dass die Größe der Zeichenfelder wie erwartet sind. Diese Methoden haben alle beschreibende Namen und folgen demselben Muster:

python
# Get an author object to test
author = Author.objects.get(id=1)

# Get the metadata for the required field and use it to query the required field data
field_label = author._meta.get_field('first_name').verbose_name

# Compare the value to the expected result
self.assertEqual(field_label, 'first name')

Die interessanten Dinge, die man beachten sollte, sind:

  • Wir können den verbose_name nicht direkt mit author.first_name.verbose_name abrufen, weil author.first_name ein String ist (kein Handle auf das first_name-Objekt, das wir verwenden können, um auf seine Eigenschaften zuzugreifen). Stattdessen müssen wir das Attribut _meta des Autors verwenden, um eine Instanz des Feldes zu erhalten und dieses zu verwenden, um nach den zusätzlichen Informationen zu fragen.
  • Wir entschieden uns, assertEqual(field_label,'first name') anstelle von assertTrue(field_label == 'first name') zu verwenden. Der Grund dafür ist, dass wenn der Test fehlschlägt, die Ausgabe des ersteren Ihnen sagt, was die Bezeichnung tatsächlich war, was das Debugging des Problems nur ein wenig erleichtert.

Hinweis: Tests für die last_name und date_of_birth Labels und auch der Test für die Länge des last_name-Feldes wurden weggelassen. Fügen Sie jetzt Ihre eigenen Versionen hinzu, indem Sie den oben gezeigten Namenskonventionen und Ansätzen folgen.

Wir müssen auch unsere benutzerdefinierten Methoden testen. Diese überprüfen im Wesentlichen nur, ob der Objektname wie erwartet im Format "Nachname", "Vorname" konstruiert wurde und dass die URL, die wir für ein Author-Element erhalten, so ist, wie wir es erwarten würden.

python
def test_object_name_is_last_name_comma_first_name(self):
    author = Author.objects.get(id=1)
    expected_object_name = f'{author.last_name}, {author.first_name}'
    self.assertEqual(str(author), expected_object_name)

def test_get_absolute_url(self):
    author = Author.objects.get(id=1)
    # This will also fail if the urlconf is not defined.
    self.assertEqual(author.get_absolute_url(), '/catalog/author/1')

Führen Sie jetzt die Tests aus. Wenn Sie das Autor-Modell so erstellt haben, wie wir es im Modelltutorial beschrieben haben, ist es sehr wahrscheinlich, dass Sie einen Fehler für die date_of_death-Bezeichnung, wie unten gezeigt, erhalten. Der Test schlägt fehl, weil er geschrieben wurde, in der Annahme, dass die Bezeichnungsdefinition der Django-Konvention folgt, den ersten Buchstaben der Bezeichnung nicht zu kapitalisieren (Django übernimmt das für Sie).

bash
======================================================================
FAIL: test_date_of_death_label (catalog.tests.test_models.AuthorModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\...\locallibrary\catalog\tests\test_models.py", line 32, in test_date_of_death_label
    self.assertEqual(field_label,'died')
AssertionError: 'Died' != 'died'
- Died
? ^
+ died
? ^

Dies ist ein sehr kleiner Fehler, aber er zeigt auf, wie das Schreiben von Tests jegliche Annahmen, die Sie möglicherweise gemacht haben, gründlicher überprüfen kann.

Hinweis: Ändern Sie die Bezeichnung für das date_of_death-Feld (/catalog/models.py) in "died" und führen Sie die Tests erneut aus.

Die Muster für das Testen der anderen Modelle sind ähnlich, daher werden wir sie nicht weiter diskutieren. Fühlen Sie sich frei, Ihre eigenen Tests für unsere anderen Modelle zu erstellen.

Formulare

Die Philosophie für das Testen Ihrer Formulare ist dieselbe wie für das Testen Ihrer Modelle; Sie müssen alles testen, was Sie kodiert haben oder was Ihr Design vorschreibt, aber nicht das Verhalten des zugrunde liegenden Frameworks und anderer Drittanbieter-Bibliotheken.

In der Regel bedeutet dies, dass Sie testen sollten, dass die Formulare die Felder haben, die Sie möchten, und dass diese mit den entsprechenden Labels und Hilfetexten angezeigt werden. Sie müssen nicht überprüfen, dass Django den Feldtyp korrekt validiert (es sei denn, Sie haben Ihr eigenes benutzerdefiniertes Feld und die Validierung erstellt) – d.h. Sie müssen nicht testen, dass ein E-Mail-Feld nur E-Mails akzeptiert. Sie müssen jedoch jede zusätzliche Validierung testen, die Sie für die Felder erwarten und alle Nachrichten, die Ihr Code bei Fehlern generieren wird.

Betrachten Sie unser Formular zur Erneuerung von Büchern. Es hat nur ein Feld für das Erneuerungsdatum, das wir mit einem Label und einem Hilfetext anzeigen müssen, die wir überprüfen werden.

python
class RenewBookForm(forms.Form):
    """Form for a librarian to renew books."""
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

    def clean_renewal_date(self):
        data = self.cleaned_data['renewal_date']

        # Check if a date is not in the past.
        if data < datetime.date.today():
            raise ValidationError(_('Invalid date - renewal in past'))

        # Check if date is in the allowed range (+4 weeks from today).
        if data > datetime.date.today() + datetime.timedelta(weeks=4):
            raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

        # Remember to always return the cleaned data.
        return data

Öffnen Sie unsere Datei /catalog/tests/test_forms.py und ersetzen Sie vorhandenen Code durch den folgenden Testcode für das RenewBookForm-Formular. Wir beginnen, indem wir unser Formular und einige Python- und Django-Bibliotheken importieren, um zeitbezogene Funktionalität zu testen. Dann deklarieren wir unsere Testklasse auf die gleiche Weise wie bei den Modellen und verwenden einen beschreibenden Namen für unsere TestCase-abgeleitete Testklasse.

python
import datetime

from django.test import TestCase
from django.utils import timezone

from catalog.forms import RenewBookForm

class RenewBookFormTest(TestCase):
    def test_renew_form_date_field_label(self):
        form = RenewBookForm()
        self.assertTrue(form.fields['renewal_date'].label is None or form.fields['renewal_date'].label == 'renewal date')

    def test_renew_form_date_field_help_text(self):
        form = RenewBookForm()
        self.assertEqual(form.fields['renewal_date'].help_text, 'Enter a date between now and 4 weeks (default 3).')

    def test_renew_form_date_in_past(self):
        date = datetime.date.today() - datetime.timedelta(days=1)
        form = RenewBookForm(data={'renewal_date': date})
        self.assertFalse(form.is_valid())

    def test_renew_form_date_too_far_in_future(self):
        date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1)
        form = RenewBookForm(data={'renewal_date': date})
        self.assertFalse(form.is_valid())

    def test_renew_form_date_today(self):
        date = datetime.date.today()
        form = RenewBookForm(data={'renewal_date': date})
        self.assertTrue(form.is_valid())

    def test_renew_form_date_max(self):
        date = timezone.localtime() + datetime.timedelta(weeks=4)
        form = RenewBookForm(data={'renewal_date': date})
        self.assertTrue(form.is_valid())

Die ersten beiden Funktionen testen, ob das label und der help_text des Feldes wie erwartet sind. Wir müssen auf das Feld über das Feld-Dictionary zugreifen (z. B. form.fields['renewal_date']). Beachten Sie hier, dass wir auch überprüfen müssen, ob der Labelwert None ist, weil Django zwar das korrekte Label rendert, aber None zurückgibt, wenn der Wert nicht explizit gesetzt ist.

Die restlichen Funktionen testen, ob das Formular für Erneuerungsdaten, die sich innerhalb des akzeptablen Bereichs befinden, gültig ist und für Werte außerhalb des Bereichs ungültig ist. Beachten Sie, wie wir Testdatumwerte um unser aktuelles Datum (datetime.date.today()) mithilfe von datetime.timedelta() (in diesem Fall durch Angabe einer Anzahl von Tagen oder Wochen) konstruieren. Dann erstellen wir einfach das Formular, indem wir unsere Daten übergeben und testen, ob es gültig ist.

Hinweis: Hier verwenden wir weder die Datenbank noch den Testclient. Erwägen Sie, diese Tests zu ändern, um den SimpleTestCase zu verwenden.

Wir müssen auch validieren, dass die korrekten Fehler erzeugt werden, wenn das Formular ungültig ist, jedoch wird dies normalerweise als Teil der Ansichtverarbeitung behandelt, sodass wir das im nächsten Abschnitt erledigen.

Warnung: Wenn Sie die ModelForm-Klasse RenewBookModelForm(forms.ModelForm) anstelle der Klasse RenewBookForm(forms.Form) verwenden, dann wäre der Formularfeldname 'due_back' statt 'renewal_date'.

Das war's zu Formularen; wir haben einige andere, aber sie werden automatisch von unseren generischen klassenbasierten Bearbeitungsansichten erstellt und sollten dort getestet werden! Führen Sie die Tests durch und bestätigen Sie, dass unser Code immer noch erfolgreich ist!

Ansichten

Um unser Anzeigeverhalten zu validieren, verwenden wir den Django-Test-Client. Diese Klasse fungiert wie ein Dummy-Webbrowser, den wir verwenden können, um GET- und POST-Anfragen zu einer URL zu simulieren und die Antwort zu beobachten. Wir können fast alles über die Antwort sehen, von niedrigstufigem HTTP (Ergebnisheader und Statuscodes) bis zur Vorlage, die wir verwenden, um das HTML zu rendern, und die Kontextdaten, die wir ihr übergeben. Wir können auch die Umleitungskette (falls vorhanden) sehen und die URL und den Statuscode an jedem Schritt überprüfen. Dies ermöglicht es uns, zu überprüfen, ob jede Ansicht das tut, was erwartet wird.

Lassen Sie uns mit einer unserer einfachsten Ansichten beginnen, die eine Liste aller Autoren bereitstellt. Diese wird bei der URL /catalog/authors/ angezeigt (eine URL, die in der URL-Konfiguration als 'authors' bezeichnet wird).

python
class AuthorListView(generic.ListView):
    model = Author
    paginate_by = 10

Da dies eine generische Listenansicht ist, wird fast alles von Django für uns erledigt. Man könnte argumentieren, dass wenn Sie Django vertrauen, dann das Einzige, was Sie testen müssen, ist, dass die Ansicht unter der korrekten URL erreichbar ist und über ihren Namen aufgerufen werden kann. Wenn Sie jedoch einen testgetriebenen Entwicklungsprozess verwenden, beginnen Sie mit dem Schreiben von Tests, die bestätigen, dass die Ansicht alle Autoren anzeigt, sie in Abschnitten von 10 paginiert.

Öffnen Sie die Datei /catalog/tests/test_views.py und ersetzen Sie vorhandenen Text durch den folgenden Testcode für AuthorListView. Wie zuvor importieren wir unser Modell und einige nützliche Klassen. In der Methode setUpTestData() richten wir eine Anzahl von Author-Objekten ein, damit wir unsere Paginierung testen können.

python
from django.test import TestCase
from django.urls import reverse

from catalog.models import Author

class AuthorListViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Create 13 authors for pagination tests
        number_of_authors = 13

        for author_id in range(number_of_authors):
            Author.objects.create(
                first_name=f'Dominique {author_id}',
                last_name=f'Surname {author_id}',
            )

    def test_view_url_exists_at_desired_location(self):
        response = self.client.get('/catalog/authors/')
        self.assertEqual(response.status_code, 200)

    def test_view_url_accessible_by_name(self):
        response = self.client.get(reverse('authors'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('authors'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'catalog/author_list.html')

    def test_pagination_is_ten(self):
        response = self.client.get(reverse('authors'))
        self.assertEqual(response.status_code, 200)
        self.assertTrue('is_paginated' in response.context)
        self.assertTrue(response.context['is_paginated'] == True)
        self.assertEqual(len(response.context['author_list']), 10)

    def test_lists_all_authors(self):
        # Get second page and confirm it has (exactly) remaining 3 items
        response = self.client.get(reverse('authors')+'?page=2')
        self.assertEqual(response.status_code, 200)
        self.assertTrue('is_paginated' in response.context)
        self.assertTrue(response.context['is_paginated'] == True)
        self.assertEqual(len(response.context['author_list']), 3)

Alle Tests verwenden den Client (der zu unserer von TestCase abgeleiteten Klasse gehört), um eine GET-Anfrage zu simulieren und eine Antwort zu erhalten. Die erste Version überprüft eine spezifische URL (nur der spezifische Pfad ohne die Domäne), während die zweite die URL aus dem Namen in der URL-Konfiguration generiert.

python
response = self.client.get('/catalog/authors/')
response = self.client.get(reverse('authors'))

Sobald wir die Antwort haben, fragen wir sie nach ihrem Statuscode, der verwendeten Vorlage, ob die Antwort paginiert ist, die Anzahl der zurückgegebenen Elemente und die Gesamtanzahl der Elemente ab.

Hinweis: Wenn Sie die Variable paginate_by in Ihrer Datei /catalog/views.py auf eine andere Zahl als 10 gesetzt haben, stellen Sie sicher, die Zeilen zu aktualisieren, die testen, ob die korrekte Anzahl von Elementen in paginierten Vorlagen oben und in den folgenden Abschnitten angezeigt wird. Wenn Sie z. B. die Variable für die Autorenlistenseite auf 5 setzen, aktualisieren Sie die Zeile oben zu:

python
self.assertTrue(len(response.context['author_list']) == 5)

Die interessanteste Variable, die wir oben demonstrieren, ist response.context, die die Kontextvariable ist, die von der Ansicht an die Vorlage übergeben wird. Dies ist unglaublich nützlich für das Testen, weil es uns erlaubt zu bestätigen, dass unsere Vorlage alle Daten erhält, die sie benötigt. Mit anderen Worten, wir können überprüfen, ob wir die beabsichtigte Vorlage verwenden und welche Daten die Vorlage erhält, was einen großen Beitrag dazu leistet, sicherzustellen, dass alle Renderprobleme nur auf die Vorlage zurückzuführen sind.

Ansichten, die auf eingeloggte Benutzer beschränkt sind

In einigen Fällen möchten Sie eine Ansicht testen, die nur für eingeloggte Benutzer eingeschränkt ist. Zum Beispiel ist unsere LoanedBooksByUserListView sehr ähnlich zu unserer vorherigen Ansicht, aber nur für eingeloggte Benutzer zugänglich und zeigt nur BookInstance-Datensätze an, die vom aktuellen Benutzer ausgeliehen wurden, den Status „verliehen“ haben und „älteste zuerst“ geordnet sind.

python
from django.contrib.auth.mixins import LoginRequiredMixin

class LoanedBooksByUserListView(LoginRequiredMixin, generic.ListView):
    """Generic class-based view listing books on loan to current user."""
    model = BookInstance
    template_name ='catalog/bookinstance_list_borrowed_user.html'
    paginate_by = 10

    def get_queryset(self):
        return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')

Fügen Sie den folgenden Testcode zu /catalog/tests/test_views.py hinzu. Hier verwenden wir zuerst SetUp(), um einige Benutzeranmeldekonten und BookInstance-Objekte (zusammen mit ihren zugehörigen Büchern und anderen Datensätzen) zu erstellen, die wir später in den Tests verwenden werden. Die Hälfte der Bücher ist von jedem Testbenutzer ausgeliehen, wir haben jedoch ursprünglich den Status aller Bücher auf „Wartung“ gesetzt. Wir haben SetUp() anstelle von setUpTestData() verwendet, weil wir einige dieser Objekte später ändern werden.

Hinweis: Der setUp()-Code unten erstellt ein Buch mit einer bestimmten Language, aber Ihr Code enthält möglicherweise nicht das Language-Modell, da dies als Herausforderung erstellt wurde. Kommentieren Sie in diesem Fall die Teile des Codes aus, die Language-Objekte erstellen oder importieren. Sie sollten dies auch im Abschnitt RenewBookInstancesViewTest tun, der folgt.

python
import datetime

from django.utils import timezone

# Get user model from settings
from django.contrib.auth import get_user_model
User = get_user_model()

from catalog.models import BookInstance, Book, Genre, Language

class LoanedBookInstancesByUserListViewTest(TestCase):
    def setUp(self):
        # Create two users
        test_user1 = User.objects.create_user(username='testuser1', password='1X<ISRUkw+tuK')
        test_user2 = User.objects.create_user(username='testuser2', password='2HJ1vRV0Z&3iD')

        test_user1.save()
        test_user2.save()

        # Create a book
        test_author = Author.objects.create(first_name='John', last_name='Smith')
        test_genre = Genre.objects.create(name='Fantasy')
        test_language = Language.objects.create(name='English')
        test_book = Book.objects.create(
            title='Book Title',
            summary='My book summary',
            isbn='ABCDEFG',
            author=test_author,
            language=test_language,
        )

        # Create genre as a post-step
        genre_objects_for_book = Genre.objects.all()
        test_book.genre.set(genre_objects_for_book) # Direct assignment of many-to-many types not allowed.
        test_book.save()

        # Create 30 BookInstance objects
        number_of_book_copies = 30
        for book_copy in range(number_of_book_copies):
            return_date = timezone.localtime() + datetime.timedelta(days=book_copy%5)
            the_borrower = test_user1 if book_copy % 2 else test_user2
            status = 'm'
            BookInstance.objects.create(
                book=test_book,
                imprint='Unlikely Imprint, 2016',
                due_back=return_date,
                borrower=the_borrower,
                status=status,
            )

    def test_redirect_if_not_logged_in(self):
        response = self.client.get(reverse('my-borrowed'))
        self.assertRedirects(response, '/accounts/login/?next=/catalog/mybooks/')

    def test_logged_in_uses_correct_template(self):
        login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK')
        response = self.client.get(reverse('my-borrowed'))

        # Check our user is logged in
        self.assertEqual(str(response.context['user']), 'testuser1')
        # Check that we got a response "success"
        self.assertEqual(response.status_code, 200)

        # Check we used correct template
        self.assertTemplateUsed(response, 'catalog/bookinstance_list_borrowed_user.html')

Um zu überprüfen, ob die Ansicht auf eine Login-Seite umleitet, wenn der Benutzer nicht eingeloggt ist, verwenden wir assertRedirects, wie es in test_redirect_if_not_logged_in() gezeigt wird. Um zu überprüfen, ob die Seite für einen eingeloggt Nutzer angezeigt wird, loggen wir uns zuerst mit unserem Testbenutzer ein und greifen dann erneut auf die Seite zu und überprüfen, ob wir einen status_code von 200 (Erfolg) erhalten.

Der Rest der Tests überprüft, dass unsere Ansicht nur Bücher zurückgibt, die an unseren aktuellen Entleiher ausgeliehen sind. Kopieren Sie den unten stehenden Code und fügen Sie ihn am Ende der obigen Testklasse ein.

python
    def test_only_borrowed_books_in_list(self):
        login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK')
        response = self.client.get(reverse('my-borrowed'))

        # Check our user is logged in
        self.assertEqual(str(response.context['user']), 'testuser1')
        # Check that we got a response "success"
        self.assertEqual(response.status_code, 200)

        # Check that initially we don't have any books in list (none on loan)
        self.assertTrue('bookinstance_list' in response.context)
        self.assertEqual(len(response.context['bookinstance_list']), 0)

        # Now change all books to be on loan
        books = BookInstance.objects.all()[:10]

        for book in books:
            book.status = 'o'
            book.save()

        # Check that now we have borrowed books in the list
        response = self.client.get(reverse('my-borrowed'))
        # Check our user is logged in
        self.assertEqual(str(response.context['user']), 'testuser1')
        # Check that we got a response "success"
        self.assertEqual(response.status_code, 200)

        self.assertTrue('bookinstance_list' in response.context)

        # Confirm all books belong to testuser1 and are on loan
        for book_item in response.context['bookinstance_list']:
            self.assertEqual(response.context['user'], book_item.borrower)
            self.assertEqual(book_item.status, 'o')

    def test_pages_ordered_by_due_date(self):
        # Change all books to be on loan
        for book in BookInstance.objects.all():
            book.status='o'
            book.save()

        login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK')
        response = self.client.get(reverse('my-borrowed'))

        # Check our user is logged in
        self.assertEqual(str(response.context['user']), 'testuser1')
        # Check that we got a response "success"
        self.assertEqual(response.status_code, 200)

        # Confirm that of the items, only 10 are displayed due to pagination.
        self.assertEqual(len(response.context['bookinstance_list']), 10)

        last_date = 0
        for book in response.context['bookinstance_list']:
            if last_date == 0:
                last_date = book.due_back
            else:
                self.assertTrue(last_date <= book.due_back)
                last_date = book.due_back

Sie könnten auch Paginierungstests hinzufügen, falls Sie dies wünschen!

Testen von Ansichten mit Formularen

Das Testen von Ansichten mit Formularen ist etwas komplizierter als in den obigen Fällen, weil Sie mehr Codepfade testen müssen: anfängliche Anzeige, Anzeige nach dem Fehlschlagen der Datenvalidierung und Anzeige nach erfolgreicher Validierung. Die gute Nachricht ist, dass wir den Client zum Testen fast genau auf die gleiche Weise wie bei reinen Anzeigeansichten verwenden.

Um dies zu demonstrieren, lassen Sie uns einige Tests für die Ansicht zum Erneuern von Büchern (renew_book_librarian()) schreiben:

python
from catalog.forms import RenewBookForm

@permission_required('catalog.can_mark_returned')
def renew_book_librarian(request, pk):
    """View function for renewing a specific BookInstance by librarian."""
    book_instance = get_object_or_404(BookInstance, pk=pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        book_renewal_form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_instance.due_back = form.cleaned_data['renewal_date']
            book_instance.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed'))

    # If this is a GET (or any other method) create the default form
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        book_renewal_form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})

    context = {
        'book_renewal_form': book_renewal_form,
        'book_instance': book_instance,
    }

    return render(request, 'catalog/book_renew_librarian.html', context)

Wir müssen testen, dass die Ansicht nur für Benutzer verfügbar ist, die die Berechtigung can_mark_returned haben, und dass Benutzer auf eine HTTP 404-Fehlerseite umgeleitet werden, wenn sie versuchen, einen nicht existierenden BookInstance zu erneuern. Wir sollten überprüfen, dass der anfängliche Wert des Formulars mit einem Datum in drei Wochen in der Zukunft vorbelegt ist, und dass wir, wenn die Validierung erfolgreich ist, zur Ansicht "alle ausgeliehenen Bücher" umgeleitet werden. Im Rahmen der Überprüfung der Validierungsfehler-Tests werden wir auch prüfen, ob unser Formular die entsprechenden Fehlermeldungen sendet.

Fügen Sie den ersten Teil der Testklasse (wie unten gezeigt) unten in /catalog/tests/test_views.py hinzu. Dies erstellt zwei Benutzer und zwei Buchinstanzen, aber nur einem Benutzer wird die für den Zugriff auf die Ansicht erforderliche Berechtigung eingeräumt.

python
import uuid

from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned.

class RenewBookInstancesViewTest(TestCase):
    def setUp(self):
        # Create a user
        test_user1 = User.objects.create_user(username='testuser1', password='1X<ISRUkw+tuK')
        test_user2 = User.objects.create_user(username='testuser2', password='2HJ1vRV0Z&3iD')

        test_user1.save()
        test_user2.save()

        # Give test_user2 permission to renew books.
        permission = Permission.objects.get(name='Set book as returned')
        test_user2.user_permissions.add(permission)
        test_user2.save()

        # Create a book
        test_author = Author.objects.create(first_name='John', last_name='Smith')
        test_genre = Genre.objects.create(name='Fantasy')
        test_language = Language.objects.create(name='English')
        test_book = Book.objects.create(
            title='Book Title',
            summary='My book summary',
            isbn='ABCDEFG',
            author=test_author,
            language=test_language,
        )

        # Create genre as a post-step
        genre_objects_for_book = Genre.objects.all()
        test_book.genre.set(genre_objects_for_book) # Direct assignment of many-to-many types not allowed.
        test_book.save()

        # Create a BookInstance object for test_user1
        return_date = datetime.date.today() + datetime.timedelta(days=5)
        self.test_bookinstance1 = BookInstance.objects.create(
            book=test_book,
            imprint='Unlikely Imprint, 2016',
            due_back=return_date,
            borrower=test_user1,
            status='o',
        )

        # Create a BookInstance object for test_user2
        return_date = datetime.date.today() + datetime.timedelta(days=5)
        self.test_bookinstance2 = BookInstance.objects.create(
            book=test_book,
            imprint='Unlikely Imprint, 2016',
            due_back=return_date,
            borrower=test_user2,
            status='o',
        )

Fügen Sie die folgenden Tests unten in die Testklasse ein. Diese überprüfen, dass nur Benutzer mit den richtigen Berechtigungen (testuser2) auf die Ansicht zugreifen können. Wir überprüfen alle Fälle: den Fall, dass der Benutzer nicht eingeloggt ist, wenn ein Benutzer eingeloggt ist, aber nicht die richtigen Berechtigungen hat, wenn der Benutzer Berechtigungen hat, aber nicht der Entleiher ist (sollte erfolgreich sein) und was passiert, wenn sie versuchen, auf eine nicht existierende BookInstance zuzugreifen. Wir überprüfen auch, dass die richtige Vorlage verwendet wird.

python
   def test_redirect_if_not_logged_in(self):
        response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}))
        # Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
        self.assertEqual(response.status_code, 302)
        self.assertTrue(response.url.startswith('/accounts/login/'))

    def test_forbidden_if_logged_in_but_not_correct_permission(self):
        login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK')
        response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}))
        self.assertEqual(response.status_code, 403)

    def test_logged_in_with_permission_borrowed_book(self):
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance2.pk}))

        # Check that it lets us login - this is our book and we have the right permissions.
        self.assertEqual(response.status_code, 200)

    def test_logged_in_with_permission_another_users_borrowed_book(self):
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}))

        # Check that it lets us login. We're a librarian, so we can view any users book
        self.assertEqual(response.status_code, 200)

    def test_HTTP404_for_invalid_book_if_logged_in(self):
        # unlikely UID to match our bookinstance!
        test_uid = uuid.uuid4()
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        response = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid}))
        self.assertEqual(response.status_code, 404)

    def test_uses_correct_template(self):
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}))
        self.assertEqual(response.status_code, 200)

        # Check we used correct template
        self.assertTemplateUsed(response, 'catalog/book_renew_librarian.html')

Fügen Sie die nächste Testmethode hinzu, wie unten gezeigt. Diese überprüft, dass das Anfangsdatum des Formulars drei Wochen in der Zukunft liegt. Beachten Sie, wie wir den Wert der Anfangswerte des Formularfeldes (response.context['form'].initial['renewal_date'])) zugreifen können.

python
    def test_form_renewal_date_initially_has_date_three_weeks_in_future(self):
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}))
        self.assertEqual(response.status_code, 200)

        date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3)
        self.assertEqual(response.context['form'].initial['renewal_date'], date_3_weeks_in_future)

Der nächste Test (fügen Sie diesen auch zur Klasse hinzu) überprüft, dass die Ansicht zu einer Liste aller ausgeliehenen Bücher weiterleitet, wenn die Erneuerung erfolgreich ist. Was hier erstmals abweicht ist, dass wir zeigen, wie Sie mit dem Client daten senden („POST“-Anfragen) können. Die Daten des POST sind das zweite Argument der post-Funktion und werden als Verzeichnis von Schlüsseln/Werten angegeben.

python
    def test_redirects_to_all_borrowed_book_list_on_success(self):
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2)
        response = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future})
        self.assertRedirects(response, reverse('all-borrowed'))

Warnung: Die alle-ausgeliehenen Ansicht wurde als Herausforderung hinzugefügt und Ihr Code könnte stattdessen zur Startseite '/' umleiten. Wenn das der Fall ist, passen Sie die letzten beiden Zeilen des Testcodes an, damit sie in etwa wie der untenstehende Code aussehen. Das follow=True in der Anfrage stellt sicher, dass die Anfrage die finale Ziel-URL zurückgibt (daher wird das Testen von /catalog/ anstelle von /).

python
 response = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future}, follow=True)
 self.assertRedirects(response, '/catalog/')

Kopieren Sie die letzten beiden Funktionen in die Klasse, wie unten zu sehen. Diese werden von POST-Anfragen geprüft, jedoch in diesem Fall mit ungültigen Erneuerungsdaten. Wir verwenden assertFormError(), um zu überprüfen, dass die Fehlermeldungen wie erwartet sind.

python
    def test_form_invalid_renewal_date_past(self):
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        date_in_past = datetime.date.today() - datetime.timedelta(weeks=1)
        response = self.client.post(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}), {'renewal_date': date_in_past})
        self.assertEqual(response.status_code, 200)
        self.assertFormError(response.context['form'], 'renewal_date', 'Invalid date - renewal in past')

    def test_form_invalid_renewal_date_future(self):
        login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD')
        invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5)
        response = self.client.post(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}), {'renewal_date': invalid_date_in_future})
        self.assertEqual(response.status_code, 200)
        self.assertFormError(response.context['form'], 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')

Die gleichen Arten von Techniken können verwendet werden, um die anderen Ansichten zu testen.

Vorlagen

Django stellt Test-APIs bereit, um zu überprüfen, dass die korrekte Vorlage von Ihren Ansichten aufgerufen wird, und ermöglicht es Ihnen, zu überprüfen, dass die richtigen Informationen gesendet werden. Es gibt jedoch keine spezifische API-Unterstützung zum Testen in Django, um zu überprüfen, dass Ihr HTML-Output wie erwartet gerendert wird.

Andere empfohlene Testwerkzeuge

Das Testframework von Django kann Ihnen helfen, effektive Unit- und Integrationstests zu schreiben – wir haben nur einen Eindruck davon vermittelt, was das zugrundeliegende unittest-Framework leisten kann, ganz zu schweigen von den Ergänzungen von Django (sehen Sie sich beispielsweise an, wie Sie unittest.mock verwenden können, um Drittanbieter-Bibliotheken zu patchen, damit Sie Ihren eigenen Code gründlicher testen können).

Es gibt zahlreiche weitere Testwerkzeuge, die Sie verwenden können, aber wir heben nur zwei hervor:

  • Coverage: Dieses Python-Tool berichtet darüber, wie viel Ihres Codes tatsächlich von Ihren Tests ausgeführt wird. Es ist besonders nützlich, wenn Sie gerade erst anfangen und versuchen, herauszufinden, was Sie testen sollten.
  • Selenium ist ein Framework, um das Testen in einem echten Browser zu automatisieren. Es ermöglicht Ihnen, einen echten Benutzer zu simulieren, der mit der Site interagiert, und bietet ein großartiges Framework für Systemtests Ihrer Site (der nächste Schritt nach den Integrationstests).

Fordern Sie sich selbst heraus

Es gibt noch viele weitere Modelle und Ansichten, die wir testen können. Versuchen Sie als Herausforderung, einen Testfall für die AuthorCreate-Ansicht zu erstellen.

python
class AuthorCreate(PermissionRequiredMixin, CreateView):
    model = Author
    fields = ['first_name', 'last_name', 'date_of_birth', 'date_of_death']
    initial = {'date_of_death': '11/11/2023'}
    permission_required = 'catalog.add_author'

Denken Sie daran, dass Sie alles überprüfen müssen, was Sie spezifizieren oder das Teil des Designs ist. Dies wird beinhalten, wer Zugriff hat, das Anfangsdatum, die verwendete Vorlage und wohin die Ansicht bei Erfolg umleitet.

Sie könnten den folgenden Code verwenden, um Ihren Test einzurichten und Ihrem Benutzer die entsprechenden Berechtigungen zuzuweisen

python
class AuthorCreateViewTest(TestCase):
    """Test case for the AuthorCreate view (Created as Challenge)."""

    def setUp(self):
        # Create a user
        test_user = User.objects.create_user(
            username='test_user', password='some_password')

        content_typeAuthor = ContentType.objects.get_for_model(Author)
        permAddAuthor = Permission.objects.get(
            codename="add_author",
            content_type=content_typeAuthor,
        )

        test_user.user_permissions.add(permAddAuthor)
        test_user.save()

Zusammenfassung

Das Schreiben von Testcode ist weder unterhaltsam noch glamourös und bleibt daher häufig beim Erstellen einer Website auf der Strecke (oder wird gar nicht gemacht). Es ist jedoch ein wesentlicher Bestandteil, um sicherzustellen, dass Ihr Code sicher ist, nachdem Änderungen vorgenommen wurden, und kosteneffektiv zu warten.

In diesem Tutorial haben wir Ihnen gezeigt, wie Sie Tests für Ihre Modelle, Formulare und Ansichten schreiben und ausführen können. Am wichtigsten haben wir eine kurze Zusammenfassung gegeben, was Sie testen sollten, was oft das Schwierigste ist, herauszufinden, wenn Sie gerade erst anfangen. Es gibt viel mehr zu wissen, aber selbst mit dem, was Sie bereits gelernt haben, sollten Sie in der Lage sein, effektive Unit-Tests für Ihre Websites zu erstellen.

Das nächste und letzte Tutorial zeigt, wie Sie Ihre wundervolle (und vollständig getestete!) Django-Website bereitstellen können.

Siehe auch