Написание вашего первого приложения Django, часть 5

Этот учебник начинается с того места, где остановился Урок 4 . Мы создали приложение для веб-опросов, и теперь мы создадим для него несколько автоматических тестов.

Где получить помощь:

Если у вас возникли проблемы с прохождением этого руководства, перейдите в раздел « Получение справки » в FAQ.

Представляем автоматизированное тестирование

Что такое автоматизированные тесты?

Тесты - это процедуры, которые проверяют работу вашего кода.

Тестирование работает на разных уровнях. Некоторые тесты могут применяться к мельчайшим деталям ( возвращает ли конкретный метод модели ожидаемые значения? ), В то время как другие исследуют общую работу программного обеспечения ( дает ли последовательность действий пользователя на сайте желаемый результат? ). Это не отличается от того типа тестирования, который вы проводили ранее в Уроке 2 , используя shellдля изучения поведения метода или при запуске приложения и вводе данных для проверки его поведения.

Отличие автоматизированных тестов заключается в том, что работа по тестированию выполняется за вас системой. Вы создаете набор тестов один раз, а затем, когда вы вносите изменения в свое приложение, вы можете убедиться, что ваш код по-прежнему работает так, как вы изначально планировали, без необходимости выполнять трудоемкое ручное тестирование.

Зачем нужно создавать тесты

Так зачем создавать тесты и почему именно сейчас?

Вы можете почувствовать, что у вас на тарелке достаточно просто изучения Python / Django, а еще что-то, что нужно изучить и сделать, может показаться ошеломляющим и, возможно, ненужным. В конце концов, наше приложение для опросов сейчас работает вполне нормально; трудности с созданием автоматических тестов не улучшат его работу. Если создание приложения для опросов - это последний этап программирования на Django, которым вы когда-либо будете заниматься, то это правда, вам не нужно знать, как создавать автоматические тесты. Но если это не так, сейчас отличное время для обучения.

Тесты сэкономят ваше время

До определенного момента «проверка работоспособности» будет удовлетворительным тестом. В более сложном приложении у вас могут быть десятки сложных взаимодействий между компонентами.

Изменение любого из этих компонентов может иметь неожиданные последствия для поведения приложения. Проверка того, что он все еще «кажется, работает», может означать проработку функциональности вашего кода с двадцатью различными вариантами ваших тестовых данных, чтобы убедиться, что вы что-то не сломали, - не лучшее использование вашего времени.

Это особенно верно, когда автоматизированные тесты могут сделать это за вас за секунды. Если что-то пошло не так, тесты также помогут определить код, вызывающий неожиданное поведение.

Иногда может показаться утомительным оторваться от продуктивной творческой работы по программированию и столкнуться с негласным и неинтересным делом написания тестов, особенно когда вы знаете, что ваш код работает правильно.

Однако задача написания тестов намного эффективнее, чем тратить часы на тестирование приложения вручную или попытки определить причину вновь возникшей проблемы.

Тесты не просто выявляют проблемы, они предотвращают их

Ошибочно думать о тестах как о негативном аспекте разработки.

Без тестов цель или предполагаемое поведение приложения может быть довольно непонятным. Даже если это ваш собственный код, вы иногда будете ковыряться в нем, пытаясь выяснить, что именно он делает.

Тесты меняют это; они освещают ваш код изнутри, и когда что-то идет не так, они фокусируют свет на той части, которая пошла не так, даже если вы даже не осознавали, что что-то пошло не так .

Тесты делают ваш код более привлекательным

Возможно, вы создали блестящую программу, но вы обнаружите, что многие другие разработчики откажутся смотреть на нее, потому что в ней отсутствуют тесты; без тестов им не поверят. Джейкоб Каплан-Мосс, один из первых разработчиков Django, говорит: «Код без тестов нарушен по замыслу».

То, что другие разработчики хотят видеть тесты в вашем программном обеспечении, прежде чем они начнут серьезно к нему относиться, - еще одна причина для вас начать писать тесты.

Тесты помогают командам работать вместе

Предыдущие пункты написаны с точки зрения одного разработчика, обслуживающего приложение. Сложные приложения будут обслуживаться командами. Тесты гарантируют, что коллеги случайно не сломают ваш код (и что вы не нарушите их код, не зная об этом). Если вы хотите зарабатывать на жизнь программистом Django, вы должны уметь писать тесты!

Базовые стратегии тестирования

Есть много способов подойти к написанию тестов.

Некоторые программисты следуют дисциплине, называемой «разработка через тестирование »; они фактически пишут свои тесты до того, как напишут свой код. Это может показаться нелогичным, но на самом деле это похоже на то, что большинство людей будет делать в любом случае: они описывают проблему, а затем создают код для ее решения. Разработка через тестирование формализует проблему в тестовом примере Python.

Чаще всего новичок в тестировании создает код, а затем решает, что в нем должны быть тесты. Возможно, лучше было бы написать несколько тестов раньше, но начать никогда не поздно.

Иногда сложно понять, с чего начать писать тесты. Если вы написали несколько тысяч строк на Python, выбор чего-то для тестирования может оказаться непростым. В таком случае будет полезно написать свой первый тест в следующий раз, когда вы внесете изменения, будь то добавление новой функции или исправление ошибки.

Так что давайте сделаем это прямо сейчас.

Написание нашего первого теста

Выявляем ошибку

К счастью, в приложении есть небольшая ошибка, которую pollsмы должны исправить прямо сейчас: Question.was_published_recently()метод возвращает, Trueесли Questionбыло опубликовано в течение последнего дня (что правильно), но также и если поле Question's pub_dateнаходится в будущем (что, конечно, не так) .

Подтвердите ошибку, используя shellдля проверки метода вопрос, дата которого находится в будущем:

$ python manage.py shell
...\> py manage.py shell
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # 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

Поскольку будущее не является «недавним», это явно неверно.

Создайте тест, чтобы выявить ошибку

То, что мы только что сделали shellдля проверки проблемы, - это именно то, что мы можем сделать в автоматизированном тесте, поэтому давайте превратим его в автоматический тест.

Стандартное место для тестов приложения - tests.pyфайл приложения ; система тестирования автоматически найдет тесты в любом файле, имя которого начинается с test.

В tests.pyфайле 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)

Здесь мы создали django.test.TestCaseподкласс с методом, который создает Questionэкземпляр с a pub_dateв будущем. Затем мы проверяем вывод was_published_recently()- который должен быть ложным.

Запуск тестов

В терминале мы можем запустить наш тест:

$ python manage.py test polls
...\> py manage.py test polls

и вы увидите что-то вроде:

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

Другая ошибка?

Если вместо этого вы получаете NameErrorздесь, возможно, вы пропустили шаг в Части 2, где мы добавили импорт datetimeи timezoneв polls/models.py. Скопируйте импорт из этого раздела и попробуйте снова запустить тесты.

Произошло вот что:

  • manage.py test pollsискал тесты в pollsприложении
  • он нашел подкласс django.test.TestCaseкласса
  • он создал специальную базу данных с целью тестирования
  • он искал методы тестирования - те, чьи имена начинаются с test
  • в test_was_published_recently_with_future_questionнем создан Question экземпляр, pub_dateполе которого на 30 дней вперед
  • … И, используя assertIs()метод, он обнаружил, что он was_published_recently()возвращает True, хотя мы хотели, чтобы он возвращал False

Тест сообщает нам, какой тест не удался и даже линия, на которой произошел сбой.

Исправляем ошибку

Мы уже знаем, в чем проблема: Question.was_published_recently()должны вернуться, Falseесли она возникнет pub_dateв будущем. Измените метод в models.pyтак, чтобы он возвращался только в том Trueслучае, если дата также в прошлом:

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

и снова запустим тест:

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

После выявления ошибки мы написали тест, который выявляет ее, и исправили ошибку в коде, чтобы наш тест прошел.

Многие другие вещи могут пойти не так с нашим приложением в будущем, но мы можем быть уверены, что случайно не введем эту ошибку повторно, потому что запуск теста немедленно предупредит нас. Мы можем считать эту небольшую часть приложения надежно закрепленной навсегда.

Более подробные тесты

Пока мы здесь, мы можем подробнее определить was_published_recently() метод; Фактически, было бы положительно неловко, если бы при исправлении одной ошибки мы ввели другую.

Добавьте еще два тестовых метода в тот же класс, чтобы более полно проверить поведение метода:

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)

И теперь у нас есть три теста, которые подтверждают, что Question.was_published_recently() возвращают разумные значения для прошлых, недавних и будущих вопросов.

Опять же, pollsэто минимальное приложение, но каким бы сложным оно ни разрасталось в будущем и с каким бы другим кодом оно не взаимодействовало, теперь у нас есть некоторая гарантия того, что метод, для которого мы написали тесты, будет вести себя ожидаемым образом.

Проверьте вид

Приложение для проведения опросов довольно неразборчиво: оно публикует любые вопросы, в том числе те, чья pub_dateобласть находится в будущем. Мы должны это улучшить. Установка pub_dateв будущем должна означать, что Вопрос будет опубликован в этот момент, но невидим до тех пор.

Тест на вид

Когда мы исправили ошибку выше, мы сначала написали тест, а затем код для ее исправления. Фактически, это был пример разработки через тестирование, но на самом деле не имеет значения, в каком порядке мы выполняем работу.

В нашем первом тесте мы сосредоточились на внутреннем поведении кода. В этом тесте мы хотим проверить его поведение, которое будет восприниматься пользователем через веб-браузер.

Прежде чем мы попытаемся что-либо исправить, давайте взглянем на инструменты, имеющиеся в нашем распоряжении.

Тестовый клиент Django

Django предоставляет тест Clientдля имитации взаимодействия пользователя с кодом на уровне представления. Мы можем использовать его в tests.py или даже в shell.

Мы начнем снова с shell, где нам нужно сделать пару вещей, в которых нет необходимости tests.py. Первый - настроить тестовую среду в shell:

$ python manage.py shell
...\> py manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

setup_test_environment()устанавливает средство визуализации шаблонов, которое позволяет нам проверять некоторые дополнительные атрибуты ответов, response.contextкоторые в противном случае были бы недоступны. Обратите внимание, что этот метод не настраивает тестовую базу данных, поэтому следующее будет выполняться для существующей базы данных, и результат может немного отличаться в зависимости от того, какие вопросы вы уже создали. Вы можете получить неожиданный результат , если ваш TIME_ZONEв settings.pyэто не правильно. Если вы не помните, что устанавливали его ранее, проверьте его, прежде чем продолжить.

Затем нам нужно импортировать тестовый клиентский класс (позже tests.pyмы будем использовать django.test.TestCaseкласс, который поставляется со своим собственным клиентом, поэтому это не потребуется):

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

Когда все будет готово, мы можем попросить клиента поработать за нас:

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

Улучшение нашего обзора

В списке опросов отображаются опросы, которые еще не опубликованы (т. Е. Те, у которых есть значок pub_dateв будущем). Давай исправим это.

В Уроке 4 мы представили представление на основе классов, основанное на 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]

Нам нужно изменить get_queryset()метод и изменить его так, чтобы он также проверял дату, сравнивая ее с timezone.now(). Сначала нам нужно добавить импорт:

polls / views.py
from django.utils import timezone

а затем мы должны изменить get_querysetметод следующим образом:

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())возвращает набор запросов, содержащий Questions, значение которого pub_dateменьше или равно - то есть раньше или равно - timezone.now.

Тестируем наш новый взгляд

Теперь вы можете убедиться, что это работает, как ожидалось, запустив runserver, загрузив сайт в свой браузер, создав Questionsс датами в прошлом и будущем и проверив, что перечислены только те, которые были опубликованы. Вам не нужно делать это каждый раз, когда вы вносите какие-либо изменения, которые могут повлиять на это, поэтому давайте также создадим тест на основе нашего shellсеанса выше.

Добавьте следующее в polls/tests.py:

polls / tests.py
from django.urls import reverse

и мы создадим функцию быстрого доступа для создания вопросов, а также новый тестовый класс:

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

Давайте рассмотрим некоторые из них более внимательно.

Во-первых, это функция быстрого доступа к вопросу, create_questionчтобы исключить повторение из процесса создания вопросов.

test_no_questionsне создает вопросов, но проверяет сообщение: «Опросы недоступны». и проверяет, latest_question_listпусто. Обратите внимание, что django.test.TestCaseкласс предоставляет некоторые дополнительные методы утверждения. В этих примерах мы используем assertContains()и assertQuerysetEqual().

В test_past_question, мы создаем вопрос и проверяем, что он отображается в списке.

В test_future_question, мы создаем вопрос с a pub_dateв будущем. База данных сбрасывается для каждого метода тестирования, поэтому первого вопроса больше нет, и снова в индексе не должно быть никаких вопросов.

И так далее. Фактически, мы используем тесты, чтобы рассказать историю действий администратора и взаимодействия с пользователем на сайте, и проверяем, что в каждом состоянии и при каждом новом изменении состояния системы публикуются ожидаемые результаты.

Тестирование DetailView

То, что у нас есть, работает хорошо; однако, даже если будущие вопросы не появятся в указателе , пользователи все равно могут связаться с ними, если они знают или угадывают правильный URL. Поэтому нам нужно добавить аналогичное ограничение к 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())

Затем мы должны добавить несколько тестов, чтобы проверить, может ли отображаться объект Question, который pub_dateнаходится в прошлом, а тот, который pub_dateв будущем - нет:

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)

Идеи для дополнительных тестов

Мы должны добавить аналогичный get_querysetметод ResultsViewи создать новый тестовый класс для этого представления. Это будет очень похоже на то, что мы только что создали; на самом деле будет много повторений.

Мы могли бы также улучшить наше приложение другими способами, добавляя тесты по ходу дела. Например, глупо, что Questionsна сайте можно публиковать, у которого нет Choices. Таким образом, наши представления могут проверить это и исключить такие Questions. Наши тесты бы создать Questionбез , Choicesа затем проверить , что это не опубликовано, а также создать подобное Question с Choices , и испытание , что он будет опубликован.

Возможно, авторизованным администраторам следует разрешить видеть неопубликованных Questions, но не обычных посетителей. Опять же: все, что необходимо добавить в программное обеспечение для достижения этой цели, должно сопровождаться тестом, независимо от того, напишете ли вы сначала тест, а затем заставите код пройти тест, или сначала проработаете логику в своем коде, а затем напишите тест для Докажите это.

В определенный момент вы обязательно посмотрите на свои тесты и задаетесь вопросом, не страдает ли ваш код от раздутия тестов, что приводит нас к:

При тестировании больше - лучше

Может показаться, что наши тесты выходят из-под контроля. При такой скорости в наших тестах скоро будет больше кода, чем в нашем приложении, и повторение неэстетично по сравнению с элегантной лаконичностью остальной части нашего кода.

Неважно . Пусть растут. По большей части вы можете написать тест один раз, а потом забыть о нем. Он будет продолжать выполнять свою полезную функцию по мере того, как вы продолжаете развивать свою программу.

Иногда тесты нужно будет обновить. Предположим, мы изменяем наши взгляды так, что публикуются только Questionsс Choices. В этом случае многие из наших существующих тестов потерпят неудачу, сообщая нам, какие именно тесты необходимо изменить, чтобы обновить их , так что в этом случае тесты помогают позаботиться о себе сами.

В худшем случае, продолжая разработку, вы можете обнаружить, что у вас есть некоторые тесты, которые теперь являются избыточными. Даже это не проблема; в тестировании избыточность - это хорошо .

Пока ваши тесты организованы разумно, они не станут неуправляемыми. Хорошие практические правила включают:

  • отдельный TestClassдля каждой модели или вида
  • отдельный метод тестирования для каждого набора условий, которые вы хотите протестировать
  • имена методов тестирования, описывающие их функции

Дальнейшее тестирование

В этом руководстве представлены только некоторые основы тестирования. Вы можете сделать гораздо больше, и в вашем распоряжении ряд очень полезных инструментов для достижения некоторых очень умных вещей.

Например, хотя наши тесты здесь охватили некоторую внутреннюю логику модели и то, как наши представления публикуют информацию, вы можете использовать «встроенный в браузер» фреймворк, такой как Selenium, для проверки того, как ваш HTML-код действительно отображается в браузере. Эти инструменты позволяют вам проверять не только поведение вашего кода Django, но также, например, вашего JavaScript. Приятно видеть, как тесты запускают браузер и начинают взаимодействовать с вашим сайтом, как если бы им управлял человек! Django включает LiveServerTestCase в себя для облегчения интеграции с такими инструментами, как Selenium.

Если у вас сложное приложение, вы можете захотеть запускать тесты автоматически при каждой фиксации в целях непрерывной интеграции , чтобы контроль качества сам - по крайней мере частично - автоматизирован.

Хороший способ обнаружить непроверенные части вашего приложения - проверить покрытие кода. Это также помогает идентифицировать хрупкий или даже мертвый код. Если вы не можете протестировать фрагмент кода, это обычно означает, что код следует отредактировать или удалить. Покрытие поможет выявить мертвый код. См. Подробности в разделе « Интеграция с покрытием .py».

Тестирование в Django содержит исчерпывающую информацию о тестировании.

Что дальше?

Полную информацию о тестировании см. В разделе « Тестирование в Django» .

Когда вы освоитесь с тестированием представлений Django, прочитайте часть 6 этого руководства, чтобы узнать об управлении статическими файлами.

Copyright ©2021 All rights reserved