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

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

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

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

Введение в автоматическое тестирование

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Некоторые программисты следуют дисциплине, называемой «разработка через тестирование» (<http://fr.wikipedia.org/wiki/Test_Driven_Development>). Они пишут тесты до того, как начинают писать код. Это может показаться нелогичным, но это очень похожий процесс на то, что делает большинство людей: они описывают проблему, а затем пишут код для ее решения. Разработка через тестирование формализует проблему в тестовом примере Python.

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

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

Давайте углубимся в самую гущу.

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

Обнаружена ошибка

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

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

$ python manage.py shell
... \> оболочка py manage.py
>>> 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 , предоставляя информацию pub_date в будущем. Затем мы проверяем результат, was_published_recently() который должен стоить False .

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

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

$ python manage.py test polls
... \> тестовые опросы py manage.py

и вы должны увидеть что-то вроде:

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 ;
  • in test_was_published_recently_with_future_question она создала экземпляр Question , поле которого pub_date находится на 30 днях в будущем;
  • ... и, используя метод assertIs() , она обнаружила, что ее метод was_published_recently() возвращается True , тогда как мы хотели, чтобы он вернулся False .

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

Исправление ошибки

Мы уже знаем проблему: Question.was_published_recently() следует отправить повторно, False если она появится pub_date в будущем. Исправьте метод in, 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
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

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

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

Тестирование нового представления

Теперь вы можете убедиться, что все работает должным образом, запустив сайт runserver и зайдя на него из браузера. Создавайте вопросы с датами в прошлом и будущем и убедитесь, что в списке отображаются только те, которые были опубликованы. Но вы не хотите выполнять эту работу по ручному тестированию каждый раз, когда вы вносите изменения, которые могут повлиять на это поведение , поэтому давайте также создадим тест на основе содержимого из нашего 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.
        """
        create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past 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.
        """
        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: Past question.>']
        )

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

Давайте подробнее рассмотрим некоторые из этих методов.

Прежде всего, функция быстрого доступа create_question позволяет избежать повторения процесса создания вопросов несколько раз.

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

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

В test_future_question , мы создаем вопрос с 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 и создать новый тестовый класс для этого представления. Он будет очень похож на тот, который мы только что создали; на самом деле повторений будет много.

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

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

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

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

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

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

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

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

Пока ваши тесты логически построены, они не станут неуправляемыми. Следует помнить о некоторых хороших принципах:

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

Еще больше тестов

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

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

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

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

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

А потом ?

Для получения полной информации о тестировании см. Django и тестирование .

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

Copyright ©2020 All rights reserved