# Django 6.0.7 documentation

# Skriva din första Django-app, del 5

Denna handledning börjar där Tutorial 4 slutade. Vi har byggt en applikation för webbenkäter och nu ska vi skapa några automatiserade tester för den.

Var du kan få hjälp:

Om du har problem med att gå igenom den här handledningen kan du gå till avsnittet Att få hjälp i FAQ.

## Introduktion av automatiserad testning

### Vad är automatiserade tester?

Tester är rutiner som kontrollerar att din kod fungerar som den ska.

Testning sker på olika nivåer. Vissa tester kan gälla en liten detalj (återför en viss modellmetod värden som förväntat?) medan andra undersöker programvarans övergripande funktion (ger en sekvens av användarinmatningar på webbplatsen önskat resultat?). Det skiljer sig inte från den typ av testning du gjorde tidigare i Tutorial 2, där du använde `shell` för att undersöka hur en metod fungerar, eller körde programmet och matade in data för att kontrollera hur det fungerar.

Det som är annorlunda med automatiserade tester är att testarbetet görs åt dig av systemet. Du skapar en uppsättning tester en gång, och när du sedan gör ändringar i din app kan du kontrollera att din kod fortfarande fungerar som du ursprungligen avsåg, utan att behöva utföra tidskrävande manuell testning.

### Varför du behöver skapa tester

Så varför skapa tester, och varför just nu?

Du kanske tycker att du har tillräckligt att göra bara med att lära dig Python/Django, och att ha ännu en sak att lära sig och göra kan verka överväldigande och kanske onödigt. När allt kommer omkring fungerar vår pollsapplikation ganska lyckligt nu; att gå igenom besväret med att skapa automatiserade tester kommer inte att få det att fungera bättre. Om skapandet av omröstningsapplikationen är den sista delen av Django-programmering som du någonsin kommer att göra, då är det sant att du inte behöver veta hur man skapar automatiserade tester. Men om så inte är fallet är det nu en utmärkt tid att lära sig.

#### Tester sparar tid

Upp till en viss punkt är det tillräckligt att ”kontrollera att det verkar fungera”. I en mer sofistikerad applikation kan du ha dussintals komplexa interaktioner mellan komponenter.

En förändring i någon av dessa komponenter kan få oväntade konsekvenser för applikationens beteende. Att kontrollera att det fortfarande ”verkar fungera” kan innebära att du kör igenom din kods funktionalitet med tjugo olika variationer av dina testdata för att se till att du inte har brutit något - inte en bra användning av din tid.

Det är särskilt sant när automatiserade tester kan göra detta åt dig på några sekunder. Om något har gått fel hjälper testerna också till att identifiera koden som orsakar det oväntade beteendet.

Ibland kan det kännas jobbigt att slita sig från det produktiva och kreativa programmeringsarbetet för att ägna sig åt det oglamorösa och ospännande arbetet med att skriva tester, särskilt när man vet att koden fungerar som den ska.

Att skriva tester är dock mycket mer tillfredsställande än att spendera timmar på att testa din applikation manuellt eller att försöka identifiera orsaken till ett nytillkommet problem.

#### Tester identifierar inte bara problem, de förebygger dem

Det är ett misstag att bara se tester som en negativ aspekt av utveckling.

Utan tester kan syftet med eller det avsedda beteendet hos en applikation vara ganska oklart. Även om det är din egen kod kommer du ibland att behöva rota runt i den för att försöka ta reda på exakt vad den gör.

Tester ändrar på det; de lyser upp din kod från insidan, och när något går fel fokuserar de ljuset på den del som har gått fel - även om du inte ens hade insett att det hade gått fel.

#### Tester gör din kod mer attraktiv

Du kanske har skapat en briljant programvara, men du kommer att upptäcka att många andra utvecklare vägrar att titta på den eftersom den saknar tester; utan tester litar de inte på den. Jacob Kaplan-Moss, en av Djangos ursprungliga utvecklare, säger ”Code without tests is broken by design”

Att andra utvecklare vill se tester i din programvara innan de tar den på allvar är ytterligare ett skäl för dig att börja skriva tester.

#### Tester hjälper team att arbeta tillsammans

De föregående punkterna är skrivna ur en enskild utvecklares synvinkel som underhåller en applikation. Komplexa applikationer kommer att underhållas av team. Tester garanterar att kollegor inte oavsiktligt bryter din kod (och att du inte bryter deras utan att veta om det). Om du vill kunna försörja dig som Django-programmerare måste du vara bra på att skriva tester!

## Grundläggande teststrategier

Det finns många olika sätt att skriva tester på.

Vissa programmerare följer en disciplin som kallas ”testdriven utveckling”; de skriver faktiskt sina tester innan de skriver sin kod. Detta kan verka kontraintuitivt, men i själva verket liknar det vad de flesta människor ofta kommer att göra ändå: de beskriver ett problem och skapar sedan lite kod för att lösa det. Testdriven utveckling formaliserar problemet i ett Python-testfall.

Oftare skapar en nybörjare lite kod och bestämmer sig senare för att den borde ha några tester. Kanske hade det varit bättre att skriva några tester tidigare, men det är aldrig för sent att komma igång.

Ibland är det svårt att komma på var man ska börja skriva tester. Om du har skrivit flera tusen rader Python är det kanske inte så lätt att välja något att testa. I ett sådant fall är det fruktbart att skriva ditt första test nästa gång du gör en förändring, antingen när du lägger till en ny funktion eller fixar en bugg.

Så låt oss göra det på en gång.

## Skriva vårt första test

### Vi identifierar ett fel

Lyckligtvis finns det en liten bugg i applikationen `polls` som vi kan åtgärda direkt: metoden `Question.was_published_recently()` returnerar `True` om `Question` publicerades under den senaste dagen (vilket är korrekt) men också om `Question` fältet `pub_date` ligger i framtiden (vilket det verkligen inte gör).

Bekräfta felet genom att använda `shell` för att kontrollera metoden på en fråga vars datum ligger i framtiden:

/ 

    $ python manage.py shell

    ...\> py manage.py shell

    >>> import datetime
    >>> from django.utils import timezone
    >>> # create a Question instance with pub_date 30 days in the future
    >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
    >>> # was it published recently?
    >>> future_question.was_published_recently()
    True

Eftersom saker i framtiden inte är ”nyligen inträffade” är detta helt klart fel.

### Skapa ett test för att avslöja felet

Det vi just har gjort i `shell` för att testa problemet är exakt vad vi kan göra i ett automatiserat test, så låt oss göra det till ett automatiserat test.

En konventionell plats för en applikations tester är i applikationens fil `tests.py`; testsystemet hittar automatiskt tester i alla filer vars namn börjar med `test`.

Skriv följande i filen `tests.py` i applikationen `polls`:

`polls/tests.py`

    import datetime

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

    from .models import Question

    class QuestionModelTests(TestCase):
        def test_was_published_recently_with_future_question(self):
            """
            was_published_recently() returns False for questions whose pub_date
            is in the future.
            """
            time = timezone.now() + datetime.timedelta(days=30)
            future_question = Question(pub_date=time)
            self.assertIs(future_question.was_published_recently(), False)

Här har vi skapat en `django.test.TestCase`-underklass med en metod som skapar en `Question`-instans med ett `pub_date` i framtiden. Vi kontrollerar sedan resultatet av `was_published_recently()` - som bör vara False.

### Genomföra tester

I terminalen kan vi köra vårt test:

/ 

    $ python manage.py test polls

    ...\> py manage.py test polls

och du kommer att se något liknande:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    F
    ======================================================================
    FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/path/to/djangotutorial/polls/tests.py", line 16, in test_was_published_recently_with_future_question
        self.assertIs(future_question.was_published_recently(), False)
    AssertionError: True is not False

    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

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

Annat fel?

Om du istället får ett `NameError` här, kan du ha missat ett steg i Part 2 där vi lade till import av `datetime` och `timezone` till `polls/models.py`. Kopiera importerna från det avsnittet och försök köra dina tester igen.

Det som hände var följande:

- `manage.py test polls` letade efter tester i applikationen `polls`

- den hittade en underklass av `django.test.TestCase`-klassen

- skapade en särskild databas i syfte att testa

- den letade efter testmetoder - sådana vars namn börjar med `test`

- i `test_was_published_recently_with_future_question` skapades en `Question` instans vars `pub_date` fält är 30 dagar i framtiden

- … och med hjälp av metoden `assertIs()` upptäckte den att dess `was_published_recently()` returnerar `True`, även om vi ville att den skulle returnera `False`

Testet informerar oss om vilket test som misslyckades och till och med på vilken linje felet inträffade.

### Åtgärda felet

Vi vet redan vad problemet är: `Question.was_published_recently()` ska returnera `False` om dess `pub_date` ligger i framtiden. Ändra metoden i `models.py`, så att den bara returnerar `True` om datumet också är i det förflutna:

`polls/modeller.py`

    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

och kör testet igen:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    OK
    Destroying test database for alias 'default'...

Efter att ha identifierat en bugg skrev vi ett test som avslöjar den och korrigerade buggen i koden så att vårt test passerade.

Många andra saker kan gå fel med vår applikation i framtiden, men vi kan vara säkra på att vi inte oavsiktligt kommer att återinföra den här buggen, eftersom testet varnar oss omedelbart. Vi kan betrakta den här lilla delen av applikationen som säker för alltid.

### Mer omfattande tester

Medan vi är här kan vi ytterligare fastställa metoden `was_published_recently()`; det skulle faktiskt vara positivt pinsamt om vi genom att fixa en bugg hade introducerat en annan.

Lägg till ytterligare två testmetoder i samma klass för att testa metodens beteende på ett mer heltäckande sätt:

`polls/tests.py`

    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)

    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() returns True for questions whose pub_date
        is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)
        self.assertIs(recent_question.was_published_recently(), True)

Och nu har vi tre tester som bekräftar att `Question.was_published_recently()` returnerar rimliga värden för tidigare, nyligen publicerade och framtida frågor.

Återigen, `polls` är en minimal applikation, men hur komplex den än blir i framtiden och vilken annan kod den än interagerar med, har vi nu en viss garanti för att den metod vi har skrivit tester för kommer att bete sig på förväntade sätt.

## Testa en vy

Polls-programmet är ganska icke-diskriminerande: det kommer att publicera alla frågor, inklusive sådana vars `pub_date`-fält ligger i framtiden. Vi bör förbättra detta. Att ange ett `pub_date` i framtiden bör betyda att frågan publiceras vid den tidpunkten, men är osynlig fram till dess.

### Ett test för en vy

När vi åtgärdade buggen ovan skrev vi först testet och sedan koden för att åtgärda den. I själva verket var det ett exempel på testdriven utveckling, men det spelar egentligen ingen roll i vilken ordning vi gör arbetet.

I vårt första test fokuserade vi noga på det interna beteendet i koden. I det här testet vill vi kontrollera beteendet så som det skulle upplevas av en användare via en webbläsare.

Innan vi försöker fixa något, låt oss ta en titt på de verktyg vi har till vårt förfogande.

### Testklienten för Django

Django provides a test `Client` to simulate a user interacting with the code at the view level. We can use it in `tests.py` or even in the `shell`.

Vi börjar igen med `shell`, där vi behöver göra ett par saker som inte kommer att vara nödvändiga i `tests.py`. Den första är att sätta upp testmiljön i `shell`:

/ 

    $ python manage.py shell

    ...\> py manage.py shell

    >>> from django.test.utils import setup_test_environment
    >>> setup_test_environment()

`setup_test_environment()` installerar en mallåtergivare som gör att vi kan undersöka några ytterligare attribut på svar som `response.context` som annars inte skulle vara tillgängliga. Observera att den här metoden inte sätter upp en testdatabas, så följande kommer att köras mot den befintliga databasen och resultatet kan skilja sig något beroende på vilka frågor du redan har skapat. Du kan få oväntade resultat om din `TIME_ZONE` i `settings.py` inte är korrekt. Om du inte minns att du har ställt in den tidigare, kontrollera det innan du fortsätter.

Därefter måste vi importera testklientklassen (senare i `tests.py` kommer vi att använda `django.test.TestCase`-klassen, som levereras med sin egen klient, så detta kommer inte att behövas):

    >>> from django.test import Client
    >>> # create an instance of the client for our use
    >>> client = Client()

När detta är klart kan vi be kunden att göra en del arbete åt oss:

    >>> # get a response from '/'
    >>> response = client.get("/")
    Not Found: /
    >>> # we should expect a 404 from that address; if you instead see an
    >>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
    >>> # omitted the setup_test_environment() call described earlier.
    >>> response.status_code
    404
    >>> # on the other hand we should expect to find something at '/polls/'
    >>> # we'll use 'reverse()' rather than a hardcoded URL
    >>> from django.urls import reverse
    >>> response = client.get(reverse("polls:index"))
    >>> response.status_code
    200
    >>> response.content
    b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
    >>> response.context["latest_question_list"]
    <QuerySet [<Question: What's up?>]>

### Förbättra vår syn

Listan över opinionsundersökningar visar opinionsundersökningar som inte har publicerats ännu (dvs. de som har ett `pub_date` i framtiden). Låt oss fixa det.

I Tutorial 4 introducerade vi en klassbaserad vy, baserad på `ListView`:

`polls/views.py`

    class IndexView(generic.ListView):
        template_name = "polls/index.html"
        context_object_name = "latest_question_list"

        def get_queryset(self):
            """Return the last five published questions."""
            return Question.objects.order_by("-pub_date")[:5]

Vi måste ändra metoden `get_queryset()` så att den även kontrollerar datumet genom att jämföra det med `timezone.now()`. Först måste vi lägga till en import:

`polls/views.py`

    from django.utils import timezone

och sedan måste vi ändra metoden `get_queryset` så här:

`polls/views.py`

    def get_queryset(self):
        """
        Return the last five published questions (not including those set to be
        published in the future).
        """
        return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[
            :5
        ]

`Question.objects.filter(pub_date__lte=timezone.now())` returnerar en frågeuppsättning som innehåller `Question` vars `pub_date` är mindre än eller lika med - det vill säga tidigare än eller lika med - `timezone.now()`.

### Testar vår nya vy

Nu kan du försäkra dig om att det här fungerar som förväntat genom att starta `runserver`, ladda webbplatsen i din webbläsare, skapa några `Question`-poster med datum i det förflutna och i framtiden, och kontrollera att endast de som har publicerats listas. Du vill inte behöva göra det varenda gång du gör någon ändring som kan påverka detta - så låt oss också skapa ett test, baserat på vår `shell`-session ovan.

Lägg till följande i `polls/tests.py`:

`polls/tests.py`

    from django.urls import reverse

och vi kommer att skapa en genvägsfunktion för att skapa frågor samt en ny testklass:

`polls/tests.py`

    def create_question(question_text, days):
        """
        Create a question with the given `question_text` and published the
        given number of `days` offset to now (negative for questions published
        in the past, positive for questions that have yet to be published).
        """
        time = timezone.now() + datetime.timedelta(days=days)
        return Question.objects.create(question_text=question_text, pub_date=time)

    class QuestionIndexViewTests(TestCase):
        def test_no_questions(self):
            """
            If no questions exist, an appropriate message is displayed.
            """
            response = self.client.get(reverse("polls:index"))
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, "No polls are available.")
            self.assertQuerySetEqual(response.context["latest_question_list"], [])

        def test_past_question(self):
            """
            Questions with a pub_date in the past are displayed on the
            index page.
            """
            question = create_question(question_text="Past question.", days=-30)
            response = self.client.get(reverse("polls:index"))
            self.assertQuerySetEqual(
                response.context["latest_question_list"],
                [question],
            )

        def test_future_question(self):
            """
            Questions with a pub_date in the future aren't displayed on
            the index page.
            """
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse("polls:index"))
            self.assertContains(response, "No polls are available.")
            self.assertQuerySetEqual(response.context["latest_question_list"], [])

        def test_future_question_and_past_question(self):
            """
            Even if both past and future questions exist, only past questions
            are displayed.
            """
            question = create_question(question_text="Past question.", days=-30)
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse("polls:index"))
            self.assertQuerySetEqual(
                response.context["latest_question_list"],
                [question],
            )

        def test_two_past_questions(self):
            """
            The questions index page may display multiple questions.
            """
            question1 = create_question(question_text="Past question 1.", days=-30)
            question2 = create_question(question_text="Past question 2.", days=-5)
            response = self.client.get(reverse("polls:index"))
            self.assertQuerySetEqual(
                response.context["latest_question_list"],
                [question2, question1],
            )

Låt oss titta närmare på några av dessa.

Den första är en genvägsfunktion för frågor, `create_question`, som gör att processen för att skapa frågor inte behöver upprepas.

`test_no_questions` doesn’t create any questions, but checks the message: ”No polls are available.” and verifies the `latest_question_list` is empty. Note that the `django.test.TestCase` class provides some additional assertion methods. In these examples, we use `assertContains()` and `assertQuerySetEqual()`.

I `test_past_question` skapar vi en fråga och kontrollerar att den visas i listan.

I `test_future_question` skapar vi en fråga med ett `pub_date` i framtiden. Databasen återställs för varje testmetod, så den första frågan finns inte längre kvar, och indexet bör inte heller ha några frågor i sig.

Och så vidare. I själva verket använder vi testerna för att berätta en historia om administratörsinmatning och användarupplevelse på webbplatsen, och kontrollerar att de förväntade resultaten publiceras vid varje tillstånd och för varje ny förändring i systemets tillstånd.

### Testning av `DetailView`

Det vi har fungerar bra, men även om framtida frågor inte visas i index kan användarna fortfarande nå dem om de känner till eller gissar rätt URL. Så vi måste lägga till en liknande begränsning till `DetailView`:

`polls/views.py`

    class DetailView(generic.DetailView):
        ...

        def get_queryset(self):
            """
            Excludes any questions that aren't published yet.
            """
            return Question.objects.filter(pub_date__lte=timezone.now())

Vi bör sedan lägga till några tester för att kontrollera att en ”fråga” vars ”publiceringsdatum” ligger i det förflutna kan visas, och att en fråga med ett ”publiceringsdatum” i framtiden inte kan visas:

`polls/tests.py`

    class QuestionDetailViewTests(TestCase):
        def test_future_question(self):
            """
            The detail view of a question with a pub_date in the future
            returns a 404 not found.
            """
            future_question = create_question(question_text="Future question.", days=5)
            url = reverse("polls:detail", args=(future_question.id,))
            response = self.client.get(url)
            self.assertEqual(response.status_code, 404)

        def test_past_question(self):
            """
            The detail view of a question with a pub_date in the past
            displays the question's text.
            """
            past_question = create_question(question_text="Past Question.", days=-5)
            url = reverse("polls:detail", args=(past_question.id,))
            response = self.client.get(url)
            self.assertContains(response, past_question.question_text)

### Idéer för fler tester

Vi borde lägga till en liknande `get_queryset`-metod till `ResultsView` och skapa en ny testklass för den vyn. Den kommer att vara mycket lik det vi just har skapat; i själva verket kommer det att finnas en hel del upprepningar.

Vi kan också förbättra vår applikation på andra sätt och lägga till tester längs vägen. Det är till exempel meningslöst att en `Fråga` utan relaterad `Val` kan publiceras på webbplatsen. Så våra vyer kan kontrollera detta och utesluta sådana `Question`-objekt. Våra tester skulle skapa en `Fråga` utan en `Val`, och sedan testa att den inte publiceras, samt skapa en liknande `Fråga` * med * minst en `Val`, och testa att den * är * publicerad.

Kanske bör inloggade administratörsanvändare tillåtas se opublicerade ”frågor”, men inte vanliga besökare. Återigen: vad som än behöver läggas till i programvaran för att åstadkomma detta bör åtföljas av ett test, oavsett om du skriver testet först och sedan får koden att klara testet, eller om du först utarbetar logiken i din kod och sedan skriver ett test för att bevisa det.

Vid en viss tidpunkt måste du titta på dina tester och undra om din kod lider av testbloat, vilket leder oss till:

## Vid testning är mer bättre

Det kan tyckas att våra tester håller på att växa utom kontroll. I den här takten kommer det snart att finnas mer kod i våra tester än i vår applikation, och upprepningen är oestetisk jämfört med den eleganta koncisa resten av vår kod.

Det spelar ingen roll. Låt dem växa. För det mesta kan du skriva ett test en gång och sedan glömma bort det. Det kommer att fortsätta att utföra sin användbara funktion när du fortsätter att utveckla ditt program.

Ibland behöver tester uppdateras. Anta att vi ändrar våra vyer så att endast `Question`-poster med tillhörande `Choice`-instanser publiceras. I det fallet kommer många av våra befintliga tester att misslyckas - teller oss exakt vilka tester som behöver ändras för att få dem uppdaterade, så i den utsträckningen hjälper testerna till att ta hand om sig själva.

I värsta fall, när du fortsätter att utveckla, kan du upptäcka att du har några tester som nu är överflödiga. Inte ens det är ett problem; när det gäller testning är redundans en bra sak.

Så länge dina tester är förnuftigt arrangerade kommer de inte att bli ohanterliga. Bra tumregler inkluderar att ha:

- en separat `TestClass` för varje modell eller vy

- en separat testmetod för varje uppsättning villkor som du vill testa

- namn på testmetoder som beskriver deras funktion

## Ytterligare tester

Denna handledning introducerar bara några av grunderna i testning. Det finns mycket mer du kan göra, och ett antal mycket användbara verktyg till ditt förfogande för att uppnå några mycket smarta saker.

Till exempel, medan våra tester här har täckt en del av den interna logiken i en modell och hur våra vyer publicerar information, kan du använda ett ”in-browser”-ramverk som Selenium för att testa hur din HTML faktiskt återges i en webbläsare. Med dessa verktyg kan du inte bara kontrollera beteendet hos din Django-kod, utan också till exempel hos ditt JavaScript. Det är något alldeles extra att se testerna starta en webbläsare och börja interagera med din webbplats, som om en människa körde den! Django inkluderar `LiveServerTestCase` för att underlätta integration med verktyg som Selenium.

Om du har en komplex applikation kanske du vill köra tester automatiskt med varje commit för ”kontinuerlig integration”, så att kvalitetskontrollen i sig själv - åtminstone delvis - är automatiserad.

Ett bra sätt att upptäcka otestade delar av din applikation är att kontrollera kodtäckningen. Detta hjälper också till att identifiera bräcklig eller till och med död kod. Om du inte kan testa en del av koden betyder det vanligtvis att koden bör omarbetas eller tas bort. Täckning hjälper till att identifiera död kod. Se Integration med coverage.py för mer information.

Testing in Django har omfattande information om testning.

## Vad kommer härnäst?

För fullständig information om testning, se Testning i Django.

När du känner dig bekväm med att testa Django-vyer kan du läsa del 6 av denna handledning för att lära dig mer om hantering av statiska filer.