# Django 6.0.7 - dokumentacja

# Pisanie pierwszej aplikacji Django, część 5.

Ten tutorial zaczyna się, gdzie skończył się Tutorial 4. Zbudowaliśmy aplikację web-ankietową, teraz stworzymy dla niej kilka zautomatyzowanych testów.

Gdzie szukać pomocy:

Jeśli masz trudności w przejściu tego tutorialu, przejdź do sekcji Uzyskiwanie pomocy często zadawanych pytań.

## Wprowadzenie do zautomatyzowanych testów

### Czym są zautomatyzowane testy?

Testy są rutynami, które sprawdzają działanie twojego kodu.

Testowanie działa na różnych poziomach. Niektóre testy mogą mieć zastosowanie do małych detali (czy szczególna metoda modelu zwraca spodziewane wartości?), podczas gdy inne sprawdzają ogólne działanie software’u (czy sekwecja wejścia od użytkownika na stronie daje pożądany rezultat?). Nie różni się to niczym od rodzaju testów, które robiłeś wcześniej w Tutorialu 2, używając `shell`, by sprawdzić zachowanie metody, lub uruchamiając aplikację i wprowadzające dane, by sprawdzić, jak się zachowuje.

To, co jest innego w testach zautomatyzowanych, to to, że testowanie wykonywanie jest dla ciebie przez system. Raz tworzysz zestaw testów i później, kiedy robisz zmiany w swojej aplikacji, możesz sprawdzić, czy twój kod nadal działa jak to pierwotnie zamierzałeś, bez konieczności wykonywania pochłaniających czas testów ręcznych.

### Dlaczego potrzebujesz tworzyć testy

Więc dlatego tworzyć testy i dlaczego teraz?

Możesz mieć wrażenie, że masz wystarczająco dużo na talerzu ucząc się tylko Pythona/Django i mając znów kolejną rzecz do nauki może wydawać się przytłaczające i może niepotrzebne. Ostatecznie nasza aplikacja ankietowa pracuje teraz całkiem szczęśliwie; przechodzenie przez problem tworzenia zautomatyzowanych testów nie spowoduje, że będzie działać choć trochę lepiej. Jeśli tworzenie aplikacji ankietowej jest ostatnim programowaniem w Django, jaki kiedykolwiek zrobisz, wtedy prawda, nie potrzebujesz wiedzieć, jak robić zautomatyzowane testy. Ale jeśli to nie jest twój przypadek, teraz jest wspaniały czas do nauki.

#### Testy oszczędzą ci czas

Do pewnego momentu, „sprawdzenie czy zdaje się działać” będzie satysfakcjonującym testem. W bardziej złożonej aplikacji możesz mieć tuziny złożonych interakcji pomiędzy komponentami.

Zmiana w jakimkolwiek z tych komponentów może mieć nieprzewidziane konsekwencje w zachowaniu aplikacji. Sprawdzenie, czy nadal „zdaje się działać” może znaczyć przechodzenie przez całą funkcjonalność twojego kodu z dwudziestoma różnymi wariacjami danych testowych, by upewnić się, czy czegoś nie zepsułeś – nie jest to dobrze wykorzystany czas.

Jest to szczególnie prawdą kiedy zautomatyzowane testy mogą zrobić to za ciebie w sekundy. Jeśli coś poszło źle, testy pomogą też w zidentyfikowaniu kodu, który powoduje nieprzewidziane zachowanie.

Czasem może wydawać się żmudne odrywać się od produktywnej, kreatywnej pracy programistycznej, aby zmierzyć się z szarym i nie ekscytującym biznesem pisania testów, szczególnie kiedy wiesz, że twój kod działa dobrze.

Jednakże zadanie pisania testów jest o wiele bardziej satysfakcjonujące niż spędzanie godzin na testowaniu ręcznie swojej aplikacji lub na próbach identyfikacji przyczyny nowo-wprowadzonego problemu.

#### Testy nie tylko identyfikują problemy, one pomagają ich unikać

Błędem jest myśleć o testach głównie jako negatywnym aspekcie dewelopmentu.

Bez testów cel lub zamierzone działanie aplikacji może być raczej nieprzejrzyste. Nawet jeśli to twój własny kod, czasem okaże się, że czasem w nim węszysz próbując odkryć co dokładnie robi.

Testy to zmieniają; oświetlają twój kod z wnętrza i kiedy coś pójdzie nie tak, skupiają światłona części, która poszła źle – nawet kiedy się nie zorientowałeś, że poszło źle.

#### Testy powodują, że twój kod jest bardziej atrakcyjny

Mógłbyś stworzyć wspaniały kawałek kodu, ale okaże się, że wielu deweloperów odmówi spojrzenia na niego, bo brakuje mu testów; bez testów, nie będą mu ufać. Jacob Kaplan-Moss, jeden z pierwotnych twórców Django mówi: „Kod bez testów jest popsuty z założenia”.

To, że inni deweloperzy chcą mieć testy w twoim oprogramowaniu zanim potraktują go na poważnie jest kolejnym powodem, byś zaczął pisać testy.

#### Testy pomagają zespołom pracować razem

Poprzednie punkty są napisane z perspektywy pojedynczego dewelopera opiekującego się aplikacją. Złożone aplikacje będą utrzymywane przez zespoły. Testy gwarantują, że koledzy nie zepsują przypadkowo twojego kodu (i że ty nie zepsujesz ich bez wiedzy o tym). Jeśli chcesz utrzymywać się jako programista Django, musisz być dobry w pisaniu testów!

## Podstawowe strategie testowania

Jest wiele sposobów, w jaki można podejść do pisania testów.

Some programmers follow a discipline called „test-driven development”; they actually write their tests before they write their code. This might seem counterintuitive, but in fact it’s similar to what most people will often do anyway: they describe a problem, then create some code to solve it. Test-driven development formalizes the problem in a Python test case.

Częściej ktoś nowy w testowanie stworzy pewien kod i później zdecyduje, że powinien on mieć jakieś testy. Prawdopodobnie lepiej byłoby napisać część testów wcześniej, ale nigdy nie jest za późno, żeby zacząć to robić.

Czasem trudno jest wymyślić, od czego zacząć w pisaniu testów. Jeśli napisałeś kilka tysięcy linii Pythona, wybranie czegoś do przetestowania może nie być łatwe. W takim przypadku owocnym jest napisać pierwszy test następnym razem, gdy wprowadzisz zmianę, czy to dodajesz nową funkcjonalność, czy naprawiasz błąd.

A więc zróbmy to teraz.

## Pisanie naszego pierwszego testu

### Odnajdujemy błąd

Na szczęście w aplikacji `polls` jest mały błąd do naprawienia przez nas od razu: metoda `Question.was_published_recently()` zwraca `True` jeśli `Question` zostało opublikowane w ostatnim dniu (co jest poprawne) ale również jeśli pole `pub_date` instancji `Question` jest w przyszłości (co poprawne z pewnością nie jest).

Potwierdź buga używając `shella` i sprawdź metodę na pytaniu, którego data jest w przyszłości:

/ 

    $ 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

Ponieważ rzeczy w przyszłości nie są „recent”, to jest oczywiście źle.

### Stwórz test, aby wskazać błąd

To, co zrobiliśmy w :djadmin:`shell`u, aby sprawdzić obecność problemu jest dokładnie tym, co chcemy zrobić w zautomatyzowanym teście, więc obróćmy to w zautomatyzowany test.

Miejscem według konwencji na testy aplikacji jest plik `tests.py` aplikacji; system testowania automatycznie znajdzie testy w każdym pliku, którego nazwa zaczyna się od `test`.

Wprowadź poniższe w plik `tests.py` aplikacji `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)

Stworzyliśmy tutaj podklasę `django.test.TestCase` z metodą, która tworzy instancję `Question` z `pub_date` w przyszłości. Następnie sprawdzamy wyjście `was_published_recently()` – które powinno być False.

### Uruchomienie testów

W terminalu możemy uruchomić nasz test:

/ 

    $ python manage.py test polls

    ...\> py manage.py test polls

i zobaczysz coś takiego:

    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'...

Inny błąd?

Jeśli dostajesz tutaj `NameError`, być może ominąłeś krok w części 2, gdzie dodaliśmy importy `datetime` i `timezone` do `polls/models.py`. Skopiuj importy z tamtej sekcji i spróbuj uruchomić testy ponownie.

To, co się stało, to to:

- `manage.py test polls` wyszukał testy w aplikacji `polls`

- znalazł podklasę klasy `django.test.TestCase`

- stworzył specjalną bazę danych na potrzebę testowania

- wyszukał metody testowe – te, których nazwy zaczynają się od `test`

- w `test_was_published_recently_with_future_question` stworzył instancję `Question`, której pole `pub_date` jest 30 dni w przyszłości

- …i używając metody `assertls()` odkrył, że jej `was_published_recently()` zwraca `True`, chociaż chcieliśmy, aby zwracała `False`

Test informuje nas który test się nie powiódł i nawet w której linii ukazał się błąd.

### Naprawianie błędu

Wiemy już, co jest problemem: `Question.was_published_recently()` powinna zwracać `False` jeśli jej `pub_date` jest w przyszłości. Zmień metodę w `models.py`, aby zwracała `True` tylko jeśli data jest też w przeszłości:

`polls/models.py`

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

i uruchom test ponownie:

    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'...

Po zidentyfikowaniu buga, napisaliśmy test, który go pokazuje i poprawiliśmy błąd w kodzie tak, aby test przechodził.

Wiele innych rzeczy może pójść źle z naszą aplikacją w przyszłości, ale możemy być pewni, że nie zreintrodukujemy przypadkowo tego błędu, ponieważ uruchomienie testu błyskawicznie nas ostrzeże. Możemy traktować tę małą porcję aplikacji na zawsze jako bezpieczną.

### Obszerniejsze testy

Kiedy tu jesteśmy, możemy dalej przyprzeć do muru metodę `was_published_recently()`; w zasadzie byłoby pozytywnie zawstydzające jeśli naprawiając jeden błąd wprowadziliśmy inny.

Dodaj dwie metody testowe więcej do tej samej klasy, aby przetestować zachowanie metody bardziej wszechstronnie:

`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)

I mamy teraz trzy testy, które potwierdzają, że `Question.was_published_recently()` zwraca sensowne wartości dla przeszłych, ostatnich i przyszłych pytań.

Znowu, `polls` jest małą aplikacją, ale jakkolwiek złożona będzie w przyszłości i z jakimkolwiek kodem będzie w interakcji, mamy teraz pewną gwarancję, że metoda, dla której napisaliśmy testy będzie zachowywała się w przewidywalne sposoby.

## Przetestuj widok

Aplikacja ankietowa jest bardzo niedyskryminująca: opublikuje każde pytanie, włącznie z takimi, których pole `pub_date` wskazuje na datę w przyszłości. Powinniśmy to poprawić. Ustawienie `pub_date` w przyszłości powinno oznaczać, że Question jest publikowane dopiero w tym przyszłym momencie i niewidoczne do tego czasu.

### Test dla widoku

Kiedy naprawiliśmy powyżej błąd, napisaliśmy najpierw test i później kod, który naprawił błąd. Faktycznie to był przykład test-driven dewelopmentu, ale to nie ma wielkiego znaczenia, w jakiej kolejności wykonujemy tę pracę.

W naszym pierwszym teście skupiliśmy się bardzo na wewnętrznym zachowaniu kodu. W tym teście chcemy sprawdzić jego działanie tak, jak byłoby obserwowane przez użytkownika przez przeglądarkę internetową.

Zanim spróbujemy cokolwiek naprawić, spójrzmy na narzędzia, którymi dysponujemy.

### Klient testowy 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`.

Zaczniemy znowu w `shellu`, gdzie musimy zrobić parę rzeczy, które nie będą koniczne w `tests.py`. Pierwsze, to uruchomić środowisko testowe w `shellu`:

/ 

    $ python manage.py shell

    ...\> py manage.py shell

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

`setup_test_environment()` instaluje renderer szablonów, który pozwala nam sprawdzać niektóre dodatkowe atrybuty odpowiedzi, takie jak `response.context`, które inaczej nie byłyby dostępne. Miej na uwadze, że ta metoda nie uruchamia testowej bazy danych, więc poniższe będzie uruchamiane na istniejącej bazie danych i wynik może nieco się różnić w zależności od pytań, które dotychczas stworzyłeś. Możesz otrzymać nieprzewidywane wyniki, jeśli twoje `TIME_ZONE` w `settings.py` nie jest poprawne. Jeśli nie pamiętasz, abyś je wcześniej ustawiał, sprawdź zanim kontynuujesz.

Następnie musimy zaimportować klasę klienta testowego (poźniej w `tests.py` będziemy używać klasy `django.test.TestCase`, która ma swojego własnego klienta, więc ten nie będzie wymagany):

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

Z zaimportowaną klasą, możemy zlecić klientowi wykonanie dla nas pracy:

    >>> # 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?>]>

### Udoskonalanie naszego widoku

Lista ankiet pokazuje ankiety, które nie są jeszcze opublikowane (te, które mają `pub_date` w przyszłości). Naprawmy to.

w Tutorialu 4 wprowadziliśmy class-based view, oparty na `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]

Musimy zmienić metodę `get_queryset()` tak, aby sprawdzała też datę porównując ją z `timezone.now()`. Najpierw musimy dodać import:

`polls/views.py`

    from django.utils import timezone

i później zmienić metodę `get_queryset` w ten sposób:

`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())` returns a queryset containing `Question`s whose `pub_date` is less than or equal to - that is, earlier than or equal to - `timezone.now()`.

### Testowanie naszego nowego widoku

Now you can satisfy yourself that this behaves as expected by firing up `runserver`, loading the site in your browser, creating a few `Question` entries with dates in the past and future, and checking that only those that have been published are listed. You don’t want to have to do that every single time you make any change that might affect this - so let’s also create a test, based on our `shell` session above.

Dodaj poniższe do `polls/tests.py`:

`polls/tests.py`

    from django.urls import reverse

i stworzymy funkcję skrótową do tworzenia pytań oraz nową klasę testową:

`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],
            )

Przyjmy się dokładniej niektórym z nich.

PIerwsze to funkcja skrótowa pytań, `create_question`, wyciąga powtórzenia z procesu tworzenia pytań.

`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()`.

W `test_past_question` tworzymy pytanie i weryfikujemy, że pojawia się na liście.

W `test_future_question` tworzymy pytanie z `pub_date` w przyszłości. Baza danych jest resetowana dla każdej testowej metody, więc pierwszego pytania już tutaj nie ma i znów indeks nie powinien zawierać żadnego pytania.

I tak dalej. W efekcie używamy testów, aby opowiedzieć historię o danych wprowadzonych przez administratora i doświadczeniu użytkownika na stronie i sprawdzamy na każdym etapie i dla każdej nowej zmiany w stanie systemu, czy publikowane są oczekiwane rezultaty.

### Testowanie `DetailView`

To co mamy działa dobrze, jednakże mimo to, że przyszłe pytania nie pojawiają się w indeksie, użytkownicy wciąż mogą uzyskać do nich dostęp jeśli znają lub zgadną odpowiedni URL. Musimy więc dodać podobny warunek do `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())

Następnie powinniśmy dodać kilka testów, aby sprawdzić że `Question`, którego `pub_date` jest w przeszłości może zostać wyświetlone i takie z `pub_date` w przyszłości nie może:

`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)

### Pomysły na więcej testów

Powinniśmy dodać podobną metodę `get_queryset` do `ResultsView` i stworzyć nową klasę testową dla tego widoku. Będzie bardzo podobna do tego, co przed chwilą stworzyliśmy; tak naprawdę będzie bardzo dużo powtórzone.

We could also improve our application in other ways, adding tests along the way. For example, it’s pointless that a `Question` with no related `Choice` can be published on the site. So, our views could check for this, and exclude such `Question` objects. Our tests would create a `Question` without a `Choice`, and then test that it’s not published, as well as create a similar `Question` with at least one `Choice`, and test that it is published.

Perhaps logged-in admin users should be allowed to see unpublished `Question` entries, but not ordinary visitors. Again: whatever needs to be added to the software to accomplish this should be accompanied by a test, whether you write the test first and then make the code pass the test, or work out the logic in your code first and then write a test to prove it.

W pewnym momencie na pewno spojrzysz na swoje testy i zaczniesz zastanawiać się, czy Twój kod nie cierpi na testowe wzdęcia, co prowadzi nas do:

## Testując, im więcej tym lepiej

Może się wydawać, że nasze testy zaczynają wymykać się spod kontroli. W takim tempie wkrótce będzie więcej kodu w naszych testach niż w naszej aplikacji, a powtórzenia są nieestetyczne, w porównaniu do eleganckiej zwięzłości reszty naszego kodu.

To nie ma znaczenia. Pozwól im rosnąć. W większości przypadków możesz napisać test raz i później zapomnieć o nim. Będzie dalej wykazywał przydatność w trakcie twojej pracy nad programem.

Sometimes tests will need to be updated. Suppose that we amend our views so that only `Question` entries with associated `Choice` instances are published. In that case, many of our existing tests will fail - telling us exactly which tests need to be amended to bring them up to date, so to that extent tests help look after themselves.

W najgorszym przypadku w trakcie pracy możesz zorientować się, że masz jakieś testy, które są teraz redundantne. Nawet to nie jest problemem; w testowaniu redundancja jest rzeczą dobrą.

Tak długo jak twoje testy są sensownie ułożone, nie zaczną być nie do zarządzania. Na praktyczne zasady składa się posiadanie:

- oddzielnego `TestClass` dla każdego modelu lub widoku

- oddzielnej metody testowej dla każdego zbioru warunków, które chcesz sprawdzać

- nazw metod testowych, które opisują swoje funkcje

## Dalsze testowanie

Ten tutorial przedstawia tylko podstawy testowania. Jest bardzo wiele rzeczy, które moesz robić i parę bardzo przydatnych narzędzi do twojej dyspozycji, by osiągać sprytne rzeczy.

Na przykład, podczas gdy nasze testy tutaj pokryły część wewnętrznej logiki modelu i sposobu, w jaki nasze widoki publikują informacje, możesz użyć „przeglądarkowego” frameworku takiego jak Selenium, by testować, w jaki sposób twój HTML tak naprawdę renderuje się w przeglądarce. Takie narzędzia pozwalają na sprawdzenie nie tylko zachowania twojego kodu Django, ale też na przykład twojego JavaScriptu. Jest coś w tym, widzieć testy odpalane w przeglądarce i jak wchodzą w interakcje z twoją stroną, tak jakby robił to człowiek! Django zawiera `LiveServerTestCase`, aby ułatwić integrację z narzędziami jak Selenium.

Jeśli masz złożoną aplikację, możesz chcieć uruchamiać testy automatycznie z każdym commitem w celu utrzymania ciągłej integracji, aby kontrola jakości była – przynajmniej częściowo – zautomatyzowana.

Dobrym sposobem, aby dostrzec nietestowane części twojej aplikacji, jest sprawdzić pokrycie testami. To pomaga też znaleźć delikatny lub nawet martwy kod. Jeśli nie możesz przetestować kawałka kodu, zazwyczaj znaczy to, że powinien on zostać zrefaktorowany lub usunięty. Pokrycie pomoże ci znaleźć martwy kod. Zobacz Integration with coverage.py po szczegóły.

Testowanie w Django zawiera obszerną informację na temat testowania.

## Co dalej?

Po pełną informację na temat testowania, zobacz Testowanie w Django.

Kiedy już czujesz się dobrze z testowaniem widoków Django, przeczytaj część 6 tego tutoriala, aby nauczyć się zarządzania plikami statycznymi.