Unittest в Django: тестирование моделей
Всё, что происходит в тестах — остаётся в тестах.Для полного тестировании проекта необходимо проверить работу с БД, а для этого придётся записывать и считывать данные из базы.Чтобы не замусоривать базу данных тестовыми записями, при тестировании создаётся виртуальная база: её структура полностью повторяет структуру реальной базы Django-проекта, однако никаких данных в этой базе нет: ни постов, ни пользователей — ничего.Все данные в этой временной базе нужно создавать в процессе тестирования. Запросы при тестировании делаются именно к временной базе, основная база не затрагивается.По окончании тестирования виртуальная база автоматически удаляется.
Проект Todo
Для тестирования моделей мы подготовили новый Django-проект Todo: это небольшая программа для записи и просмотра запланированных дел.
- На главной странице проекта есть форма для сохранения задания в базе данных;
- на страницу /task/ выводится полный список запланированных дел (страница доступна только авторизованному пользователю);
- на страницах с адресом вида /task// показывается полное описание определённой задачи (страница доступна только авторизованному пользователю);
- на статичной странице /about/ выводится описание проекта.
А больше в проекте ничего нет. Склонируйте проект Todo из репозитория и запустите его на своём компьютере.
Приложение staticpages
В этом приложении нет моделей, есть одна небольшая view-функция, которая отвечает за отображение страницы /about/ .
Приложение deals
Это приложение управляет добавлением и отображением запланированных дел.View-классы приложения:
- Home() отвечает за отображение главной страницы с формой для добавления задач: страница /
- TaskList() отвечает за отображение списка запланированных задач: страница /task/
- TaskDetail() отвечает за вывод подробной информации о задаче: страница /task//
- TaskAddSuccess() выводит сообщение об успешном добавлении задачи через форму: страница /added/
Модели приложения deals:
# deals/models.py from django.db import models # Нужно установить библиотеку pytils: # pip3 install pytils from pytils.translit import slugify class Task(models.Model): title = models.CharField( 'Заголовок', max_length=100, help_text='Дайте короткое название задаче' ) text = models.TextField( 'Текст', help_text='Опишите суть задачи.' ) slug = models.SlugField( 'Адрес для страницы с задачей', max_length=100, unique=True, blank=True, help_text=( 'Укажите уникальный адрес для страницы задачи. Используйте только ' 'латиницу, цифры, дефисы и знаки подчёркивания' ) ) image = models.ImageField( 'Картинка', upload_to='tasks/', blank=True, null=True, help_text='Загрузите картинку' ) def __str__(self): return self.title # Расширение встроенного метода save(): если поле slug не заполнено - # транслитерировать в латиницу содержимое поля title, обрезать до ста знаков # и сохранить в поле slug def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title)[:100] super().save(*args, **kwargs)
Поля CharField , TextField и SlugField вам знакомы. Поле ImageField нужно для хранения картинки.
Если при создании экземпляра класса Task поле slug не было заполнено — его значение будет сгенерировано из содержимого поля title . За это отвечает доработанный метод save() .
Хозяйке на заметку: стандартный метод slugify из модуля django.utils.text не умеет работать с кириллицей. Нужно установить в виртуальное окружение пакет pytils ( pip3 install pytils ) и импортировать slugify именно из него, как это сделано в листинге deals/models.py.
Тесты модели
Тесты моделей приложения удобно хранить в одноимённом файле:
└── deals ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_forms.py │ ├── test_models.py # Тесты моделей │ ├── test_urls.py │ └── test_views.py ├── admin.py ├── forms.py ├── models.py └── views.py
Что не нужно тестировать
- Работу Python: он сотни раз протестирован и работает.
- Функциональность, предоставляемую Django: фреймворк уже проверен разработчиками.
- Подключаемые библиотеки и приложения.
Не нужно проверять, что данные поля title сохранены в базу данных как CharField или slug — как SlugField : это часть реализации Django.
Если для полей CharField или SlugField в модели указана максимальная длина поля — не стоит тестировать результат: вы впустую потратите время на проверку того, что уже отлажено при разработке Django.
Что тестировать можно, но необязательно
Для моделей пользовательских форм удобно указать человекочитаемое имя: проект будет выглядеть неопрятно, если на странице с формой возле поля ввода будет стоять лейбл title или text.
Будет правильным проверить, что вы не забыли указать читаемое название для всех полей, на основании которых генерируются формы.
Указать человекочитаемое имя для поля можно так:
class Task(models.Model): title = models.CharField( 'Заголовок', # Человекочитаемое имя (verbose) для поля max_length=100, help_text='Дайте короткое название задаче' ) .
class Task(models.Model): title = models.CharField( verbose_name='Заголовок', # Человекочитаемое имя (verbose) для поля max_length=100, help_text='Дайте короткое название задаче' ) .
Это имя будет отображаться и в админке Django, и во всех формах, связанных с моделью Task . Документация по verbose доступна на официальном сайте Django.
Аналогичная ситуация с help_text : если это поле важно — стоит проверить, что про него не забыли в коде.
Полезно проверить, что __str__ возвращает ожидаемый результат. Это мелочь, но она может быть удобна в дальнейшей разработке и поднимет code coverage. Содержимое поля __str__ отображается, например, при выводе объектов в админ-зоне или при запросе объекта из queryset.
Для тестирования моделей веб-клиент не нужен: достаточно создать запись в тестовой базе данных и проверять её через ORM.
# deals/tests/tests_models.py from django.test import TestCase from deals.models import Task class TaskModelTest(TestCase): @classmethod def setUpClass(cls): super().setUpClass() # Создаём тестовую запись в БД # и сохраняем созданную запись в качестве переменной класса cls.task = Task.objects.create( title='Заголовок тестовой задачи', text='Тестовый текст', slug='test-task' ) def test_title_label(self): """verbose_name поля title совпадает с ожидаемым.""" task = TaskModelTest.task # Получаем из свойста класса Task значение verbose_name для title verbose = task._meta.get_field('title').verbose_name self.assertEqual(verbose, 'Заголовок') def test_title_help_text(self): """help_text поля title совпадает с ожидаемым.""" task = TaskModelTest.task # Получаем из свойста класса Task значение help_text для title help_text = task._meta.get_field('title').help_text self.assertEqual(help_text, 'Дайте короткое название задаче') # Аналогичным образом можно протестировать мета-значения из полей text, slug, image def test_object_name_is_title_fild(self): """__str__ task - это строчка с содержимым task.title.""" task = TaskModelTest.task # Обратите внимание на синтаксис expected_object_name = task.title self.assertEqual(expected_object_name, str(task))
Получить поле verbose_name напрямую, через group.title.verbose_name , не получится. Ведь поле group.title содержит только строку «Тестовый заголовок» .
Для доступа до verbose_name есть другой синтаксис
group._meta.get_field('title').verbose_name
Таким же образом можно получить другие параметры, задаваемые при создании поля.Если в тестах Django вы применяете методы класса setUpClass() и tearDownClass() — обязательно вызывайте в них super(): super().setUpClass() и super().tearDownClass() .Без вызова super() все тесты сработают нормально, но вы получите ошибку:
AttributeError: type object '' has no attribute 'cls_atomics'
Эта ошибка возникает именно в Django: в Unittest для Python такой проблемы нет.
К объектам, созданным в методах класса (например, в setUpClass() ), синтаксически правильно обращаться через имя класса; например, обращение к объекту task должно выглядеть так: TaskModelTest.task .
Обращение self.task тоже сработает, если не использовать наследование с переопределением метода в дочерних классах.
Проверять field_label лучше через метод assertEquals(field_label,’Заголовок’) , а не через assertTrue(field_label == ‘Заголовок’) .
Разница в том, что при тестировании через assertTrue() в отчёте будет просто сказано, что тест провален, а при assertEquals() в консоли будет указано и актуальное значение field_label . Это облегчит задачу по отладке кода.
Циклом по тестам: стандартный метод subTest()
Тестирование каждого поля модели может превратиться в ад: в большом проекте для этого придётся писать очень много похожих тестов.
Писать долго, читать неудобно, принцип DRY нарушен — всё плохо.
В подобных ситуациях принято использовать метод subTest. Вместо написания большого количества однотипных тестов можно собрать все проверки и ожидаемые результаты в словарь — и пройти по ним циклом. Много интересного о subTest можно узнать в официальной документации.
Проверка verbose_name и help_text для нескольких полей будет выглядеть так:
# deals/tests/tests_models.py from django.test import TestCase from deals.models import Task class TaskModelTest(TestCase): @classmethod def setUpClass(cls): super().setUpClass() # Создаём тестовую запись в БД # и сохраняем созданную запись в качестве переменной класса cls.task = Task.objects.create( title='Заголовок тестовой задачи', text='Тестовый текст', slug='test-task' ) def test_verbose_name(self): """verbose_name в полях совпадает с ожидаемым.""" task = TaskModelTest.task field_verboses = < 'title': 'Заголовок', 'text': 'Текст', 'slug': 'Адрес для страницы с задачей', 'image': 'Картинка', >for field, expected_value in field_verboses.items(): with self.subTest(field=field): self.assertEqual( task._meta.get_field(field).verbose_name, expected_value) def test_help_text(self): """help_text в полях совпадает с ожидаемым.""" task = TaskModelTest.task field_help_texts = < 'title': 'Дайте короткое название задаче', 'text': 'Опишите суть задачи', 'slug': ('Укажите адрес для страницы задачи. Используйте только ' 'латиницу, цифры, дефисы и знаки подчёркивания'), 'image': 'Загрузите картинку', >for field, expected_value in field_help_texts.items(): with self.subTest(field=field): self.assertEqual( task._meta.get_field(field).help_text, expected_value)
Конструкция for field, expected_value in field_help_texts.items() распаковывает словарь field_help_texts с помощью метода items() — создаёт из него два кортежа, доступных для итерации с помощью цикла:
- кортеж field содержит ключи исходного словаря field_help_texts ;
- кортеж expected_value содержит значения исходного словаря field_help_texts .
Значения из кортежа field передаются как параметр в self.subTest(field=field) ; метод subTest() устроен так, что при падении вложенного теста в отчёт будет выведено имя того поля, на котором упал тест. Если не передать этот параметр — в отчёте будет лишь информация «какой-то subTest() упал», и искать ошибку будет неудобно. Для лучшего понимания обязательно посмотрите документацию по subTest() .
Если вам платят за каждую строчку кода, как индийским разработчикам из анекдотов — subTest() лучше не использовать, разумеется.
Что в моделях нужно тестировать обязательно
В обязательном порядке тестами должен быть покрыт весь код, который вы написали сами:
- валидация полей моделей,
- методы по работе с моделями,
и всё, что может сломать логику работы программы.В приложении deals обязательно нужно проверить, что при автоматическом создании содержимого поля slug из title текст будет правильно преобразован, а его длина будет не больше ста символов.При тестировании лучше использовать данные, похожие на настоящие. Например, для тестирования метода save() лучше создать объект, в котором поле title будет заполнено кириллицей.
# deals/tests/tests_models.py from django.test import TestCase from deals.models import Task class TaskModelTest(TestCase): @classmethod def setUpClass(cls): super().setUpClass() # Создаём тестовую запись в БД # и сохраняем созданную запись в качестве переменной класса. # Значение slug не указываем, ждём, что при создании объекта # оно создастся автоматически из title. # А title сделаем таким, чтобы после транслитерации он стал более 100 символов # (буква "ж" транслитерируется в два символа: "zh") cls.task = Task.objects.create( title='Ж'*100, text='Тестовый текст' ) def test_text_convert_to_slug(self): """Содержимое поля title преобразуется в slug.""" task = TaskModelTest.task slug = task.slug self.assertEqual(slug, 'zh'*50) def test_text_slug_max_length_not_exceed(self): """Длинный slug обрезается и не превышает max_length поля slug в модели.""" task = TaskModelTest.task max_length_slug = task._meta.get_field('slug').max_length length_slug = len(task.slug) self.assertEqual(max_length_slug, length_slug)
Очевидные бонусы
Теперь любое изменение в классе Task, затрагивающее verbose_name или max_length у полей title или slug , обрушит тесты. Если нужно внести изменения — сначала надо будет исправить тесты под новые требования, как и положено в Test-driven Development.
Похожие записи:
- Unittest в Django: тестирование URLs
- Unittest в Django: тестирование Forms
- Сериализаторы для связанных моделей
- Kittygram 2: новые возможности
Тестирование в Django
При разработке веб-приложений большинство программистов часто избегают тестирования. Я основном говорю о начинающих программистах. Да и многие , кто уже довольно долго в этой профессии и которые разрабатывают коммерческие приложения довольно часто избегают тестирования , а многие незнакомы с ней
В этой статье мы будем рассматривать тестирования для веб-приложений , которые создаются с использованием фреймворка Django
Так для чего вам нужно тестирование и зачем тратить на это время и ресурсы?
Как забавно не звучало бы на первый взгляд , но тестирование позволяет вам экономить ваше время. Как обычно происходит проверка работоспособности написанного кода, если вы не используете тестирование ? Мы обычно это делаем вручную , вводя определенные данные и смотря как работает наш код. Но со временем проект разрастается и у вас десятки компонентов , которые взаимодействуют друг с другом и измненения в одном компоненте может влиять на другие компоненты. И это будет занимать большое время при ручном тестировании системы, а некоторые ошибки вы не сможете отследить до того , пока это не окажется в продакшене. А автоматизированные тесты помогут вам отследить многие ошибки до выкладки на продакшен.
Когда вы пишите новый код, то написанные вами правильно тесты позволяют убедиться в том , что ваш код работает правильно.
Также , при измении вами старого кода или кто-то из ваших коллег изменит какую-то часть нашего приложения , то тесты покажут, поломался ли ваш функционал или нет.
Тестирование веб-приложений — очень сложная задача. Так как нам нужно протестировать обработку HTTP запросов , валидацию форм , отрисовку шаблонов. Но во фреймворке Django есть средства которые упрощают нам тестирование и в этой статье рассмотрим конкретно тестирование использованием этих средств
python manage.py startapp blog
Давайте вначале опишем наши модели для блога
from django.db import models from django.utils import timezone class Category(models.Model): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255) def __str__(self): return self.name class Meta: verbose_name = 'Категория' verbose_name_plural = 'Категории' def get_absolute_url(self): return f'/category//' class Post(models.Model): class Statuses(models.TextChoices): DRAFT = 'DRAFT', 'Черновик' PUBLISHED = 'PUBLISHED', 'Опубликован' title = models.CharField(max_length=255) slug = models.SlugField(max_length=1024) content = models.TextField() created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField() status = models.CharField(max_length=10, choices = Statuses.choices, default=Statuses.DRAFT) category = models.ForeignKey(Category, related_name='posts', on_delete=models.CASCADE) def __str__(self): return self.title class Meta: ordering = ('-created_at',) verbose_name = 'Пост' verbose_name_plural = 'Посты' @property def is_published(self): return self.status == Post.Statuses.DRAFT
Создадим миграции и применим их следующими командами:
python manage.py makemigrations python manage.py migrate
Как вы можете заметить , внутри app есть файл tests.py, где мы можем написать наши тесты для этого приложения. Для начала это приемлемый путь.

У модели Category есть метод get_absolute_url , который возвращает url с использованием поля slug. В нашем первом тесте мы проверим правильно ли работает этот метод для вновь созданного объекта модели Category. Создадим наш первый тестовый класс CategoryModelTests который наследуется от TestCase и напишем первый тест методом test_absolute_url_is_correct. Методы должны начинаться с «test_»
from django.test import TestCase from blog.models import Category class CategoryModelTests(TestCase): def test_absolute_url_is_correct(self): new_category = Category(name='Первый пост', slug='first-post') new_category.save() self.assertEqual(new_category.get_absolute_url(), '/category/first-post/')
python manage.py test
Наш тест запустился и отработал успешно

Для модели Post у нас есть метод is_published , который возвращает опубликован ли пост или нет. Этот метод неправильно работает и это сделано специально , чтобы найти неправильное поведение при тестировании. Давайте напишем новый тест для модели Post, который проверяет правильно ли работает метод is_published
from django.test import TestCase from blog.models import Category, Post class CategoryModelTests(TestCase): def test_absolute_url_is_correct(self): new_category = Category.objects.create(name='Первый пост', slug='first-post') self.assertEqual(new_category.get_absolute_url(), '/category/first-post/') class PostModelTests(TestCase): def test_is_published_post(self): category = Category.objects.create(name='Первый пост', slug='first-post') new_post = Post.objects.create(title='Первый опубликованный пост', slug='first-published-post', category=category, status=Post.Statuses.PUBLISHED, ) self.assertTrue(new_post.is_published)
После запуска тестов мы видим , что у нас запустилось два теста и один тест упал. Тут в выводе мы видим на какой строчке упал и по какой причине

Давайте пофиксим наш метод Is_published в модели Post
@property def is_published(self): return self.status == Post.Statuses.PUBLISHED
Запустим заново тесты и посмотрим на результат:

Заключение
В данной статье мы написали первые тесты для нашего проекта Django и было описано для чего нужно тестирование в проектах. Тестирвание очень важная часть разработки и не стоит им пренебрегать
Руководство часть 10: Тестирование приложений Django
Сайты, в процессе развития и разработки, становится все сложнее тестировать вручную. Кроме такого тестирования, сложными становятся внутренние взаимодействия между компонентами — внесение небольшого изменения в одной части приложения влияет на другие. При этом, чтобы все продолжало работать нужно вносить все больше и больше изменений и, желательно так, чтобы не добавлялись новые ошибки. Одним из способов который позволяет смягчить последствия добавления изменений, является внедрение в разработку автоматического тестирования — оно должно просто и надёжно запускаться каждый раз, когда вы вносите изменения в свой код. Данное руководство рассматривает вопросы автоматизации юнит-тестирования вашего сайта при помощи фреймворка Django для тестов.
| Требования: | Изучить все предыдущие темы руководства, включая Руководство Django Часть 9: Работа с формами. |
|---|---|
| Цель: | Понимать как создавать юнит тесты для сайта на основе Django. |
Обзор
LocalLibrary в настоящий момент содержит страницы для показа списков всех книг, авторов, подробной информации о книгах Book и авторах Author , а также страницу для обновления информации об экземпляре книги BookInstance и, кроме того, страницы для создания, обновления и удаления записей модели Author (и модели Book , в том случае, если вы выполнили домашнее задание в руководстве работа с формами). Даже в случае небольшого сайта, ручной переход на каждую страницу и беглая проверка того, что все работает как следует, может занять несколько минут. В процессе внесения изменений и роста сайта требуемое время для проведения проверок будет только возрастать. Если бы мы продолжили в том же духе, то в какой-то момент на проведение тестов мы тратили бы больше времени, чем на написание кода и внесение изменений.
Автоматические тесты могут серьёзно помочь нам справиться с этой проблемой! Очевидными преимуществами в таком случае являются значительно меньшие временные затраты на проведение тестов, их подробное выполнение, а кроме того, тесты имеют постоянную функциональность, или последовательность действий (человек никогда не сможет тестировать так надёжно!). В связи с быстротой их выполнения автоматические тесты можно выполнять более часто, а если они провалятся, то укажут на соответствующее место (где что-то пошло не так как ожидалось).
Кроме того, автоматические тесты могут действовать как первый «настоящий пользователь» вашего кода, заставляя вас строго следить за объявлениями и документированием поведения вашего сайта. Тесты часто являются основой для создания примеров вашего кода и документации. По этим причинам иногда некоторые процессы разработки программного обеспечения начинаются с определения тестов и их реализации, а уже после этого следует написание кода который должен иметь соответствующее поведение (так называемая разработка на основе тестов и на основе поведения).
Данное руководство показывает процесс создания автоматических тестов в Django при помощи добавления их к разработке сайта LocalLibrary.
Типы тестирования
Существует несколько типов, уровней, классификаций тестов и тестовых приёмов. Наиболее важными автоматическими тестами являются:
Проверяют функциональное поведение для отдельных компонентов, часто классов и функций.
Тесты которые воспроизводят исторические ошибки (баги). Каждый тест вначале запускается для проверки того, что баг был исправлен, а затем перезапускается для того, чтобы убедиться, что он не был внесён снова с появлением новых изменений в коде.
Проверка совместной работы групп компонентов. Данные тесты отвечают за совместную работу между компонентами, не обращая внимания на внутренние процессы в компонентах. Они проводятся как для простых групп компонентов, так и для целых веб-сайтов.
**Примечание:**К другим типам тестов относятся методы чёрного ящика, белого ящика, ручные, автоматические, канареечные (canary), дымные (smoke), соответствия (conformance), принятия (acceptance), функциональные (functional), системные (system), эффективности (performance), загрузочные (load) и стресс-тесты (stress tests).
Что Django предоставляет для тестирования?
Тестирование сайта это сложная задача, потому что она состоит их нескольких логических слоёв – от HTTP-запроса и запроса к моделям, до валидации формы и их обработки, а кроме того, рендеринга шаблонов страниц.
Django предоставляет фреймворк для создания тестов, построенного на основе иерархии классов, которые, в свою очередь, зависят от стандартной библиотеки Python unittest . Несмотря на название, данный фреймворк подходит и для юнит-, и для интеграционного тестирования. Фреймворк Django добавляет методы API и инструменты, которые помогают тестировать как веб так и, специфическое для Django, поведение. Это позволяет вам имитировать URL-запросы, добавление тестовых данных, а также проводить проверку выходных данных ваших приложений. Кроме того, Django предоставляет API (LiveServerTestCase) и инструменты для применения различных фреймворков тестирования, например вы можете подключить популярный фреймворк Selenium (en-US) для имитации поведения пользователя в реальном браузере.
Для написания теста вы должны наследоваться от любого из классов тестирования Django (или юниттеста) (SimpleTestCase, TransactionTestCase, TestCase, LiveServerTestCase), а затем реализовать отдельные методы проверки кода (тесты это функции-«утверждения», которые проверяют, что результатом выражения являются значения True или False , или что два значения равны и так далее). Когда вы запускаете тест, фреймворк выполняет соответствующие тестовые методы в вашем классе-наследнике. Методы тестирования запускаются независимо друг от друга, начиная с метода настроек и/или завершаясь методом разрушения (tear-down), определённом в классе, как показано ниже.
class YourTestClass(TestCase): def setUp(self): # Установки запускаются перед каждым тестом pass def tearDown(self): # Очистка после каждого метода pass def test_something_that_will_pass(self): self.assertFalse(False) def test_something_that_will_fail(self): self.assertTrue(False)
Самый подходящий базовый класс для большинства тестов это django.test.TestCase. Этот класс создаёт чистую базу данных перед запуском своих методов, а также запускает каждую функцию тестирования в его собственной транзакции. У данного класса также имеется тестовый Клиент, который вы можете использовать для имитации взаимодействия пользователя с кодом на уровне отображения. В следующих разделах мы сконцентрируемся на юнит-тестах, которые будут созданы на основе класса TestCase.
Примечание: Класс django.test.TestCase очень удобен, но он может приводить к замедленной работе в некоторых случаях (не для каждого теста необходимо настраивать базу данных, или имитировать взаимодействие с отображением). Когда вы познакомитесь с работой данного класса, то сможете заменить некоторые из ваших тестов на более простые классы тестирования.
Что вы должны тестировать?
Вы должны тестировать все аспекты, касающиеся вашего кода, но не библиотеки, или функциональность, предоставляемые Python, или Django.
Например, рассмотрим модель Author , определённую ниже. Вам не нужно проверять тот факт, что first_name и last_name были сохранены в базу данных как CharField , потому что за это отвечает непосредственно Django (хотя конечно, на практике в течение разработки вы косвенно будете проверять данную функциональность). Тоже касается и, например, проверки того, что поле date_of_birth является датой, поскольку это тоже часть реализации Django.
Вы должны проверить текст для меток (First name, Last_name, Date of birth, Died), и размер поля, выделенного для текста (100 символов), потому что они являются частью вашей разработки и чем-то, что может сломаться/измениться в будущем.
class Author(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) date_of_death = models.DateField('Died', null=True, blank=True) def get_absolute_url(self): return reverse('author-detail', args=[str(self.id)]) def __str__(self): return '%s, %s' % (self.last_name, self.first_name)
Подобным же образом вы должны убедиться, что методы get_absolute_url() и __str__() ведут себя как требуется, потому что они являются частью вашей бизнес логики. В случае функции get_absolute_url() вы можете быть уверены, что функция из Django reverse() была реализована правильно и, следовательно, вы тестируете только то, чтобы соответствующий вызов в отображении был правильно определён.
Примечание: Проницательные читатели могут заметить, что мы можем некоторым образом ограничить дату рождения и смерти какими-то граничными значениями и выполнять проверку, чтобы дата смерти шла после рождения. В Django данное ограничение может быть добавлено к вашим классам форм (хотя вы и можете определить валидаторы для этих полей, они будут проявлять себя только на уровне форм, а не уровне модели).
Ну что же, усвоив данную информацию, давайте перейдём к процессу определения и запуска тестов.
Обзор структуры тестов
Перед тем как мы перейдём к тому «что тестировать», давайте кратко взглянем на моменты где и как определяются тесты.
Django использует юнит-тестовый модуль — встроенный «обнаружитель» тестов, который находит тесты в текущей рабочей директории, в любом файле с шаблонным именем test*.py. Предоставляя соответствующие имена файлов, вы можете работать с любой структурой которая вас устраивает. Мы рекомендуем создать пакет для вашего тестирующего кода и, следовательно, отделить файлы моделей, отображений, форм и любые другие, от кода который будет использоваться для тестов. Например:
catalog/ /tests/ __init__.py test_models.py test_forms.py test_views.py
В проекте LocalLibrary создайте файловую структуру, указанную выше. Файл __init__.py должен быть пустым (так мы говорим Питону, что данная директория является пакетом). Вы можете создать три тестовых файла при помощи копирования и переименования файла-образца /catalog/tests.py.
Примечание: Скелет тестового файла /catalog/tests.py был создан автоматически когда мы выполняли построение скелета сайта Django. Является абсолютно «легальным» действием — поместить все ваши тесты в данный файл, тем не менее, если вы проводите тесты «правильно», то вы очень быстро придёте к очень большому и неуправляемому файлу тестирования.
Можете удалить данный файл, поскольку больше он нам не понадобится.
Откройте /catalog/tests/test_models.py. Файл должен импортировать django.test.TestCase , как показано ниже:
from django.test import TestCase # Поместите ваш код тестов здесь
Вы часто будете добавлять соответствующий тестовый класс для каждой модели/отображения/формы с отдельными методами проверки каждой отдельной функциональности. В каких-то случаях вы захотите иметь отдельный класс для тестирования какого-то особого варианта работы, или функциональности, с отдельными функциями тестирования, которые будут проверять элемент/элементы данного варианта (например, мы можем создать отдельный класс тестирования для проверки того, что поле валидно, — функции данного класса будут проверять каждый неверный вариант использования). Опять же, структура файлов и пакетов полностью зависит от вас и будет лучше если вы будете её придерживаться.
Добавьте тестовый класс, показанный ниже, в нижнюю часть файла. Данный класс демонстрирует как создать класс тестирования при помощи наследования от TestCase .
class YourTestClass(TestCase): @classmethod def setUpTestData(cls): print("setUpTestData: Run once to set up non-modified data for all class methods.") pass def setUp(self): print("setUp: Run once for every test method to setup clean data.") pass def test_false_is_false(self): print("Method: test_false_is_false.") self.assertFalse(False) def test_false_is_true(self): print("Method: test_false_is_true.") self.assertTrue(False) def test_one_plus_one_equals_two(self): print("Method: test_one_plus_one_equals_two.") self.assertEqual(1 + 1, 2)
Этот класс определяет два метода которые вы можете использовать для дотестовой настройки (например, создание какой-либо модели, или других объектов, которые вам понадобятся):
- setUpTestData() вызывается каждый раз перед запуском теста на уровне настройки всего класса. Вы должны использовать данный метод для создания объектов, которые не будут модифицироваться/изменяться в каком-либо из тестовых методов.
- setUp() вызывается перед каждой тестовой функцией для настройки объектов, которые могут изменяться во время тестов (каждая функция тестирования будет получать «свежую» версию данных объектов).
Примечание: . Классы тестирования также содержат метод tearDown() , который мы пока не используем. Этот метод не особенно полезен для тестирования баз данных, поскольку базовый класс TestCase автоматически разрывает соединения с ними.
Далее идут несколько методов, которые используют функции Assert , проверяющие условия «истинно» (true), «ложно» (false) или равенство ( AssertTrue , AssertFalse , AssertEqual ). Если условия не выполняются как ожидалось, то это приводит к провалу теста и выводу соответствующего сообщения об ошибке на консоль.
Функции проверки утверждений AssertTrue , AssertFalse , AssertEqual реализованы в unittest. В данном фреймворке существуют и другие подобные функции, а кроме того, специфические для Django функции проверки, например, перехода из/к отображению ( assertRedirects ), проверки использования какого-то конкретного шаблона ( assertTemplateUsed ) и так далее.
Примечание: В обычной ситуации у вас нет необходимости вызывать функции print() из методов теста, как во фрагменте выше. Мы поступили так только для того, чтобы вы в консоле увидели порядок вызова тестовых функций класса.
Как запускать тесты
Простейшим способом запуска всех тестов является применение следующей команды:
test
Таким образом мы найдём в текущей директории все файлы с именем test*.py и запустим все тесты (у нас имеются несколько файлов для тестирования, но на данный момент, только /catalog/tests/test_models.py содержит какие-либо тесты). По умолчанию, тесты сообщат что-нибудь, только в случае провала.
Запустите тесты из корневой папки сайта LocalLibrary. Вы должны увидеть вывод, который похож на следующий.
>python manage.py test Creating test database for alias 'default'... setUpTestData: Run once to set up non-modified data for all class methods. setUp: Run once for every test method to setup clean data. Method: test_false_is_false. .setUp: Run once for every test method to setup clean data. Method: test_false_is_true. .setUp: Run once for every test method to setup clean data. Method: test_one_plus_one_equals_two. . ====================================================================== FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\Github\django_tmp\library_w_t_2\locallibrary\catalog\tests\tests_models.py", line 22, in test_false_is_true self.assertTrue(False) AssertionError: False is not true ---------------------------------------------------------------------- Ran 3 tests in 0.075s FAILED (failures=1) Destroying test database for alias 'default'...
Как видите, один тест провалился и мы можем точно увидеть в какой именно функции это произошло и почему (так и было задумано, поскольку False не равен True !).
Примечание: Самая важная вещь, которую нужно извлечь из тестового выхода выше, заключается в том, что это гораздо более ценно, если вы используете описательные/информативные имена для ваших объектов и методов.
Текст выделенный жирным, обычно не должен появляться в тестовом выводе (это результат работы функций print() в наших тестах). Он показывает, что вызов метода setUpTestData() происходит один раз для всего класса в целом, а вызовы setUp() осуществляются перед каждым методом.
Следующий раздел показывает как запускать отдельные тесты и как контролировать процесс вывода информации.
Ещё больше тестовой информации
Если вы желаете получать больше информации о тестах вы должны изменить значение параметра verbosity. Например, для вывода списка успешных и неуспешных тестов (и всю информацию о том, как прошла настройка базы данных) вы можете установить значение verbosity равным «2»:
test --verbosity 2
Доступными значениями для verbosity являются 0, 1 (значение по умолчанию), 2 и 3.
Запуск определённых тестов
Если вы хотите запустить подмножество тестов, тогда вам надо указать полный путь к вашему пакету, модулю/подмодулю, классу наследнику TestCase , или методу:
test catalog.tests # Run the specified module python3 manage.py test catalog.tests.test_models # Run the specified module python3 manage.py test catalog.tests.test_models.YourTestClass # Run the specified class python3 manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two # Run the specified method
Тестирование LocalLibrary
Теперь, когда мы знаем как запустить наши тесты и что именно мы должны тестировать, давайте рассмотрим некоторые практические примеры.
**Примечание:**Мы не будем расписывать все тесты, а просто покажем вам пример того, как они должны работать и что ещё вы можете с ними сделать.
Модели
Как было отмечено ранее, мы должны тестировать все то, что является частью нашего кода, а не библиотеки/код, которые уже были протестированы командами разработчиков Django, или Python.
Рассмотрим модель Author . Мы должны провести тесты текстовых меток всех полей, поскольку, даже несмотря на то, что не все они определены, у нас есть проект, в котором сказано, что все их значения должны быть заданы. Если мы не проведём их тестирование, тогда мы не будем знать, что данные метки действительно содержат необходимые значения. Мы уверены в том, что Django создаст поле заданной длины, таким образом наши тесты будут проверять нужный нам размер поля, а заодно и его содержимое.
class Author(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) date_of_death = models.DateField('Died', null=True, blank=True) def get_absolute_url(self): return reverse('author-detail', args=[str(self.id)]) def __str__(self): return '%s, %s' % (self.last_name, self.first_name)
Откройте файл /catalog/tests/test_models.py и замените все его содержимое кодом, приведённом во фрагменте для тестирования модели Author (фрагмент представлен ниже).
В первой строке мы импортируем класс TestCase , а затем наследуемся от него, создавая класс с описательным именем ( AuthorModelTest ), оно поможет нам идентифицировать места провалов в тестах во время вывода информации на консоль. Затем мы создаём метод setUpTestData() , в котором создаём объект автора, который мы будем использовать в тестах, но нигде не будем изменять.
from django.test import TestCase # Create your tests here. from catalog.models import Author class AuthorModelTest(TestCase): @classmethod def setUpTestData(cls): #Set up non-modified objects used by all test methods Author.objects.create(first_name='Big', last_name='Bob') def test_first_name_label(self): author=Author.objects.get(id=1) field_label = author._meta.get_field('first_name').verbose_name self.assertEquals(field_label,'first name') def test_date_of_death_label(self): author=Author.objects.get(id=1) field_label = author._meta.get_field('date_of_death').verbose_name self.assertEquals(field_label,'died') def test_first_name_max_length(self): author=Author.objects.get(id=1) max_length = author._meta.get_field('first_name').max_length self.assertEquals(max_length,100) def test_object_name_is_last_name_comma_first_name(self): author=Author.objects.get(id=1) expected_object_name = '%s, %s' % (author.last_name, author.first_name) self.assertEquals(expected_object_name,str(author)) def test_get_absolute_url(self): author=Author.objects.get(id=1) #This will also fail if the urlconf is not defined. self.assertEquals(author.get_absolute_url(),'/catalog/author/1')
Тесты полей проверяют значения текстовых меток ( verbose_name ), включая их ожидаемую длину. Все методы имеют описательные имена, а их логика придерживается одной и той же структуры:
# Получение объекта для тестирования author=Author.objects.get(id=1) # Получение метаданных поля для получения необходимых значений field_label = author._meta.get_field('first_name').verbose_name # Сравнить значение с ожидаемым результатом self.assertEquals(field_label,'first name')
Интересно отметить следующее:
- Мы не можем получить поле verbose_name напрямую через author.first_name.verbose_name , потому что author.first_name является строкой. Вместо этого, нам надо использовать атрибут _meta объекта автора для получения того экземпляра поля, который будет использоваться для получения дополнительной информации.
- Мы выбрали метод assertEquals(field_label,’first name’) вместо assertTrue(field_label == ‘first name’) , потому что, в случае провала теста, в выводе будет указано какое именно значение содержит метка и это немного облегчит нам задачу по отладке кода.
Примечание: Тесты для текстовых меток last_name и date_of_birth , а также тест длины поля last_name были опущены. Добавьте свою версию этих тестов, соблюдая соглашение об именовании и следуя структуре логики, представленной выше.
Кроме того, нам надо провести тесты наших собственных методов. Они просто проверяют, что имена объектов имеют следующие значения «Last Name, First Name» и что URL-адрес, по которому мы получаем экземпляр Author , такой как ожидается.
def test_object_name_is_last_name_comma_first_name(self): author=Author.objects.get(id=1) expected_object_name = '%s, %s' % (author.last_name, author.first_name) self.assertEquals(expected_object_name,str(author)) def test_get_absolute_url(self): author=Author.objects.get(id=1) #This will also fail if the urlconf is not defined. self.assertEquals(author.get_absolute_url(),'/catalog/author/1')
Теперь запустите тесты. Если вы создали модель Author, в соответствии с разделом о моделях данного руководства, то весьма вероятно, что вы получите сообщение об ошибке для метки date_of_death , как показано ниже. Тест провалился потому что, в соответствии с соглашением Django, первый символ имени метки должен быть в верхнем регистре (Django делает это автоматически).
====================================================================== FAIL: test_date_of_death_label (catalog.tests.test_models.AuthorModelTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\. \locallibrary\catalog\tests\test_models.py", line 32, in test_date_of_death_label self.assertEquals(field_label,'died') AssertionError: 'Died' != 'died' - Died ? ^ + died ? ^
Это несущественный баг, но он демонстрирует нам то, что написание тестов может более тщательно проверить все неточности, которые вы можете сделать.
**Примечание:**Измените значение метки для поля date_of_death (/catalog/models.py) на «died» и перезапустите тесты.
Тот же подход применяется к тестированию других моделей. Самостоятельно создайте свои собственные тесты для оставшихся моделей.
Формы
Смысл проведения тестов для форм тот же, что и для моделей; надо проверить весь собственный код и другие особенности проекта, но не то, что касается фреймворка, или сторонних библиотек.
В основном это означает, что вы должны протестировать то, что формы имеют соответствующие поля и что они показываются с соответствующими метками и вспомогательными текстами. Вам не надо проверять то, что Django правильно осуществляет валидацию полей (если только вы не создали своё собственное поле и валидацию) — то есть вам не надо проверять что, например, поле ввода электронного адреса принимает только электронного адреса. Но вы должны протестировать каждую дополнительную валидацию, которую вы добавляете для полей и любые сообщения, который ваш код генерирует в случае ошибок.
Рассмотрим форму для обновления книг. Она имеет только одно поле обновления даты, которое будет иметь текстовую метку и вспомогательный текст, который вам надо проверить.
class RenewBookForm(forms.Form): """ Форма обновления книг для библиотекарей """ renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).") def clean_renewal_date(self): data = self.cleaned_data['renewal_date'] #Проверка, что дата не в прошлом. if data datetime.date.today(): raise ValidationError(_('Invalid date - renewal in past')) #Если дата в "далёком" будущем (+4 недели) if data > datetime.date.today() + datetime.timedelta(weeks=4): raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead')) # Всегда надо возвращать очищенные данные. return data
Откройте файл /catalog/tests/test_forms.py и замените весь существующий в нем код, следующим кодом теста для формы RenewBookForm . Мы начали его с импорта нашей формы и некоторых библиотек Python и Django, которые помогут нам провести тесты. Затем, тем же способом как мы делали для моделей, объявляем тестовый класс нашей формы, то есть применяя описательное имя класс наследника TestCase .
from django.test import TestCase # Создайте ваши тесты здесь import datetime from django.utils import timezone from catalog.forms import RenewBookForm class RenewBookFormTest(TestCase): def test_renew_form_date_field_label(self): form = RenewBookForm() self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date') def test_renew_form_date_field_help_text(self): form = RenewBookForm() self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).') def test_renew_form_date_in_past(self): date = datetime.date.today() - datetime.timedelta(days=1) form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertFalse(form.is_valid()) def test_renew_form_date_too_far_in_future(self): date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1) form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertFalse(form.is_valid()) def test_renew_form_date_today(self): date = datetime.date.today() form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertTrue(form.is_valid()) def test_renew_form_date_max(self): date = timezone.now() + datetime.timedelta(weeks=4) form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertTrue(form.is_valid())
Первые две функции проверяют текст который должны содержать поля label и help_text . Доступ к полю мы получаем при помощи словаря (то есть, form.fields[‘renewal_date’] ). Отметим, что мы должны проверять содержит ли метка значение None , иначе в поле текста метки вы увидите » None «.
Оставшиеся функции проверяют валидность дат, то есть их нахождение внутри определённого интервала, а также невалидность для значений, которые находятся вне заданного интервала. Для получения исходного значения мы использовали функцию получения текущей даты ( datetime.date.today() ), а также функцию datetime.timedelta() (которая принимает определённое число дней, или недель). Затем мы просто создали форму, передавая ей наши данные и проверяя её на валидность.
Примечание: В данном примере мы не использовали ни базу данных, ни тестовый клиент. Рассмотрите модификацию этих тестов при помощи класса SimpleTestCase.
Нам также надо бы проверять возникновение ошибок, которые появляются если форма не валидна. Но, обычно, это относится к процессу вывода информации, таким образом, мы позаботимся об этом в следующем разделе.
На этом с формами можно закончить; у нас имеются и другие тесты, но они были созданы обобщёнными классами отображения для редактирования! Запустите тесты и убедитесь, что наш код все ещё им соответствует!
Отображения
Для проверки поведения отображения мы используем тестовый клиент Django Client. Данный класс действует как упрощённый веб-браузер который мы применяем для имитации GET и POST запросов и проверки ответов. Про ответы мы можем узнать почти все, начиная с низкоуровневого HTTP (итоговые заголовки и коды статусов) и вплоть до применяемых шаблонов, которые используются для HTML-рендера, а также контекста, который передаётся в соответствующий шаблон. Кроме того, мы можем отследить последовательность перенаправлений (если имеются), проверить URL-адреса и коды статусов на каждом шаге. Все это позволит нам проверить, что каждое отображение выполняет то, что ожидается.
Давайте начнём с одного из простейших отображений которое возвращает список всех авторов. Вы можете его увидеть по URL-адресу /catalog/authors/ (данный URL-адрес можно найти в разделе приложения catalog, в файле настроек urls.py по имени ‘authors’).
class AuthorListView(generic.ListView): model = Author paginate_by = 10
Поскольку это обобщённое отображение списка, то почти все за нас делает Django. Если вы доверяете Django, то единственной вещью, которую вам нужно протестировать, является переход к данному отображению по указанному URL-адресу. Таким образом, если вы применяете методику TDD (test-driven development, разработка через тесты), то начните проект с написания тестов, которые будут проверять, что данное отображение выводит всех авторов и, к тому же, например, блоками по 10.
Откройте файл /catalog/tests/test_views.py замените все его содержимое на следующий код теста для класса AuthorListView . Как и ранее, мы импортируем нашу модель и некоторые полезные классы. В методе setUpTestData() мы задаём число объектов класса Author которые мы тестируем при постраничном выводе.
from django.test import TestCase # Create your tests here. from catalog.models import Author from django.urls import reverse class AuthorListViewTest(TestCase): @classmethod def setUpTestData(cls): #Create 13 authors for pagination tests number_of_authors = 13 for author_num in range(number_of_authors): Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,) def test_view_url_exists_at_desired_location(self): resp = self.client.get('/catalog/authors/') self.assertEqual(resp.status_code, 200) def test_view_url_accessible_by_name(self): resp = self.client.get(reverse('authors')) self.assertEqual(resp.status_code, 200) def test_view_uses_correct_template(self): resp = self.client.get(reverse('authors')) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed(resp, 'catalog/author_list.html') def test_pagination_is_ten(self): resp = self.client.get(reverse('authors')) self.assertEqual(resp.status_code, 200) self.assertTrue('is_paginated' in resp.context) self.assertTrue(resp.context['is_paginated'] == True) self.assertTrue( len(resp.context['author_list']) == 10) def test_lists_all_authors(self): #Get second page and confirm it has (exactly) remaining 3 items resp = self.client.get(reverse('authors')+'?page=2') self.assertEqual(resp.status_code, 200) self.assertTrue('is_paginated' in resp.context) self.assertTrue(resp.context['is_paginated'] == True) self.assertTrue( len(resp.context['author_list']) == 3)
Все тесты используют клиент (принадлежащего классу TestCase , от которого мы наследовались) для имитации GET -запроса и получения ответа ( resp ). Первая версия проверяет заданный URL-адрес (заметьте, — просто определённый путь без указания домена), в то время как второй генерирует URL-адрес при помощи его имени, указанного в настройках.
= self.client.get('/catalog/authors/') resp = self.client.get(reverse('authors'))
Когда мы получаем ответ, то мы извлекаем код статуса, используемый шаблон, «включён» ли постраничный вывод, количество элементов в подмножестве (на странице) и общее число элементов.
Наиболее интересной переменной является resp.context , которая является объектом контекста, который передаётся шаблону из отображения. Он (объект контекста) очень полезен для тестов, поскольку позволяет нам убедиться, что наш шаблон получает все данные которые ему необходимы. Другими словами мы можем проверить, что мы используем правильный шаблон с данными, которые проделывают долгий путь проверок чтобы соответствовать данному шаблону.
Отображения и регистрация пользователей
В некоторых случаях вам нужно провести тесты отображений к которым имеют доступ только зарегистрированные пользователи. Например, LoanedBooksByUserListView очень похоже на наше предыдущее отображение, но доступно только для залогинившихся пользователей и показывает только те записи ( BookInstance) , которые соответствуют текущему пользователю, имеют статус ‘on loan’ (книга взята домой), а также забронированы.
from django.contrib.auth.mixins import LoginRequiredMixin class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView): """ Обобщённый класс отображения списка взятых книг текущим пользователем """ model = BookInstance template_name ='catalog/bookinstance_list_borrowed_user.html' paginate_by = 10 def get_queryset(self): return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')
Добавьте тестовый код следующего фрагмента в /catalog/tests/test_views.py. В нем, для создания нескольких аккаунтов и объектов BookInstance которые будут использоваться в дальнейших тестах, мы используем метод SetUp() (вместе с соответствующими книгами и другими записями). Половина книг бронируется тестовыми пользователями, но в начале для них всех мы устанавливаем статус «доступно». Использование метода SetUp() предпочтительнее чем setUpTestData() , поскольку в дальнейшем мы будем модифицировать некоторые объекты.
Примечание: Метод setUp() создаёт книгу с заданным языком Language , но ваш код может не включать в себя модель Language , поскольку это было домашним заданием. В таком случае просто закомментируйте соответствующие строки. Поступите также и в следующем разделе, посвящённом RenewBookInstancesViewTest.
import datetime from django.utils import timezone from catalog.models import BookInstance, Book, Genre, Language from django.contrib.auth.models import User # Необходимо для представления User как borrower class LoanedBookInstancesByUserListViewTest(TestCase): def setUp(self): # Создание двух пользователей test_user1 = User.objects.create_user(username='testuser1', password='12345') test_user1.save() test_user2 = User.objects.create_user(username='testuser2', password='12345') test_user2.save() # Создание книги test_author = Author.objects.create(first_name='John', last_name='Smith') test_genre = Genre.objects.create(name='Fantasy') test_language = Language.objects.create(name='English') test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language) # Create genre as a post-step genre_objects_for_book = Genre.objects.all() test_book.genre.set(genre_objects_for_book) # Присвоение типов many-to-many напрямую недопустимо test_book.save() # Создание 30 объектов BookInstance number_of_book_copies = 30 for book_copy in range(number_of_book_copies): return_date= timezone.now() + datetime.timedelta(days=book_copy%5) if book_copy % 2: the_borrower=test_user1 else: the_borrower=test_user2 status='m' BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status) def test_redirect_if_not_logged_in(self): resp = self.client.get(reverse('my-borrowed')) self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/') def test_logged_in_uses_correct_template(self): login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('my-borrowed')) # Проверка что пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') # Проверка ответа на запрос self.assertEqual(resp.status_code, 200) # Проверка того, что мы используем правильный шаблон self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html')
Если пользователь не залогирован то, чтобы убедиться в том что отображение перейдёт на страницу входа (логирования), мы используем метод assertRedirects , что продемонстрировано в методе test_redirect_if_not_logged_in() . Затем мы осуществляем вход для пользователя и проверяем что полученный статус status_code равен 200 (успешно).
Остальные тесты проверяют, соответственно, что наше отображение показывает только те книги которые взяты текущим пользователем. Скопируйте код, показанный ниже, в нижнюю часть предыдущего класса.
def test_only_borrowed_books_in_list(self): login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('my-borrowed')) #Проверка, что пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') #Check that we got a response "success" self.assertEqual(resp.status_code, 200) #Проверка, что изначально у нас нет книг в списке self.assertTrue('bookinstance_list' in resp.context) self.assertEqual( len(resp.context['bookinstance_list']),0) #Теперь все книги "взяты на прокат" get_ten_books = BookInstance.objects.all()[:10] for copy in get_ten_books: copy.status='o' copy.save() #Проверка, что все забронированные книги в списке resp = self.client.get(reverse('my-borrowed')) #Проверка, что пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') #Проверка успешности ответа self.assertEqual(resp.status_code, 200) self.assertTrue('bookinstance_list' in resp.context) #Подтверждение, что все книги принадлежат testuser1 и взяты "на прокат" for bookitem in resp.context['bookinstance_list']: self.assertEqual(resp.context['user'], bookitem.borrower) self.assertEqual('o', bookitem.status) def test_pages_ordered_by_due_date(self): #Изменение статуса на "в прокате" for copy in BookInstance.objects.all(): copy.status='o' copy.save() login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('my-borrowed')) #Пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') #Check that we got a response "success" self.assertEqual(resp.status_code, 200) #Подтверждение, что из всего списка показывается только 10 экземпляров self.assertEqual( len(resp.context['bookinstance_list']),10) last_date=0 for copy in resp.context['bookinstance_list']: if last_date==0: last_date=copy.due_back else: self.assertTrue(last_date copy.due_back)
Если хотите, то вы, безусловно, можете добавить тесты проверяющие постраничный вывод!
Тестирование форм и отображений
Процесс тестирования отображений с формами немного более сложен, чем в представленных ранее случаях, поскольку вам надо протестировать большее количество кода: начальное состояние показа формы, показ формы и её данных в случае ошибок, а также показ формы в случае успеха. Хорошей новостью является то, что мы применяем клиент для тестирования практически тем же способом, как мы делали это в случае отображений, которые отвечают только за вывод информации.
В качестве демонстрации давайте напишем некоторые тесты для отображения, которые отвечают за обновление книг( renew_book_librarian() ):
from .forms import RenewBookForm @permission_required('catalog.can_mark_returned') def renew_book_librarian(request, pk): """ Функция отображения обновления экземпляра BookInstance библиотекарем """ book_inst=get_object_or_404(BookInstance, pk = pk) # Если это POST-запрос, тогда обработать данные формы if request.method == 'POST': # Создать объект формы и заполнить её данными из запроса (связывание/биндинг): form = RenewBookForm(request.POST) # Проверка валидности формы: if form.is_valid(): # process the data in form.cleaned_data as required (here we just write it to the model due_back field) book_inst.due_back = form.cleaned_data['renewal_date'] book_inst.save() # переход по URL-адресу: return HttpResponseRedirect(reverse('all-borrowed') ) # Если это GET-запрос (или что-то ещё), то создаём форму по умолчанию else: proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) form = RenewBookForm(initial='renewal_date': proposed_renewal_date,>) return render(request, 'catalog/book_renew_librarian.html', 'form': form, 'bookinst':book_inst>)
Нам надо проверить что к данному отображению имеют доступ только те пользователи, которые имеют разрешение типа can_mark_returned , а кроме того, что пользователи перенаправляются на страницу ошибки HTTP 404 если они пытаются обновить экземпляр книги BookInstance , который не существует. Мы должны проверить что начальное значение формы соответствует дате через 3 недели в будущем, а также то, что если форма прошла валидацию, то мы переходим на страницу отображения книг «all-borrowed» (забронированных). Для тестов, отвечающих за проверку «провалов», мы также должны удостовериться что они отправляют соответствующие сообщения об ошибках.
В нижнюю часть файла /catalog/tests/test_views.py добавьте класс тестирования (показан во фрагменте, ниже). Он создаёт двух пользователей и два экземпляра книги, но только один пользователь получает необходимый доступ к соответствующему отображению. Код, который «присваивает» соответствующий доступ, выделен в коде жирным:
from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned. class RenewBookInstancesViewTest(TestCase): def setUp(self): #Создание пользователя test_user1 = User.objects.create_user(username='testuser1', password='12345') test_user1.save() test_user2 = User.objects.create_user(username='testuser2', password='12345') test_user2.save() permission = Permission.objects.get(name='Set book as returned') test_user2.user_permissions.add(permission) test_user2.save() #Создание книги test_author = Author.objects.create(first_name='John', last_name='Smith') test_genre = Genre.objects.create(name='Fantasy') test_language = Language.objects.create(name='English') test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,) #Создание жанра Create genre as a post-step genre_objects_for_book = Genre.objects.all() test_book.genre=genre_objects_for_book test_book.save() #Создание объекта BookInstance для для пользователя test_user1 return_date= datetime.date.today() + datetime.timedelta(days=5) self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o') #Создание объекта BookInstance для для пользователя test_user2 return_date= datetime.date.today() + datetime.timedelta(days=5) self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o')
В нижнюю часть класса тестирования добавьте следующие методы (из следующего фрагмента). Они проверяют, что только пользователь с соответствующим доступом (testuser2) имеет доступ к отображению. Мы проверяем все случаи: когда пользователь не залогинился, когда залогинился, но не имеет соответствующего доступа, когда имеет доступ, но не является заёмщиком книги (тест должен быть успешным), а также, что произойдёт если попытаться получить доступ к книге BookInstance которой не существует. Кроме того, мы проверяем то, что используется правильный (необходимый) шаблон.
def test_redirect_if_not_logged_in(self): resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable) self.assertEqual( resp.status_code,302) self.assertTrue( resp.url.startswith('/accounts/login/') ) def test_redirect_if_logged_in_but_not_correct_permission(self): login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable) self.assertEqual( resp.status_code,302) self.assertTrue( resp.url.startswith('/accounts/login/') ) def test_logged_in_with_permission_borrowed_book(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance2.pk,>) ) #Check that it lets us login - this is our book and we have the right permissions. self.assertEqual( resp.status_code,200) def test_logged_in_with_permission_another_users_borrowed_book(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) #Check that it lets us login. We're a librarian, so we can view any users book self.assertEqual( resp.status_code,200) def test_HTTP404_for_invalid_book_if_logged_in(self): import uuid test_uid = uuid.uuid4() #unlikely UID to match our bookinstance! login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':test_uid,>) ) self.assertEqual( resp.status_code,404) def test_uses_correct_template(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) self.assertEqual( resp.status_code,200) #Check we used correct template self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html')
Добавьте ещё один тестовый метод, показанный ниже. Он проверяет что начальная дата равна трём неделям в будущем. Заметьте, что мы имеем возможность получить доступ к начальному значению из поля формы (выделено жирным).
def test_form_renewal_date_initially_has_date_three_weeks_in_future(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) self.assertEqual( resp.status_code,200) date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3) self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future )
Следующий тест (тоже добавьте его в свой класс) проверяет что отображение, в случае успеха, перенаправляет пользователя к списку всех забронированных книг. Здесь мы показываем как при помощи клиента вы можете создать и передать данные в POST -запросе. Данный запрос передаётся вторым аргументом в пост-функцию и представляет из себя словарь пар ключ/значение.
def test_redirects_to_all_borrowed_book_list_on_success(self): login = self.client.login(username='testuser2', password='12345') valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2) resp = self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':valid_date_in_future> ) self.assertRedirects(resp, reverse('all-borrowed') )
Предупреждение: Вместо перехода к отображению all-borrowed, добавленного в качестве домашнего задания, вы можете перенаправить пользователя на домашнюю страницу ‘/’. В таком случае, исправьте две последние строки тестового кода на код, показанный ниже. Присваивание follow=True , в запросе, гарантирует что запрос вернёт окончательный URL-адрес пункта назначения (следовательно проверяется /catalog/ , а не / ).
= self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':valid_date_in_future>,follow=True ) self.assertRedirects(resp, '/catalog/')
Скопируйте две последние функции в класс, представленные ниже. Они тоже проверяют POST -запросы, но для случая неверных дат. Мы используем функцию assertFormError() , чтобы проверить сообщения об ошибках.
def test_form_invalid_renewal_date_past(self): login = self.client.login(username='testuser2', password='12345') date_in_past = datetime.date.today() - datetime.timedelta(weeks=1) resp = self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':date_in_past> ) self.assertEqual( resp.status_code,200) self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past') def test_form_invalid_renewal_date_future(self): login = self.client.login(username='testuser2', password='12345') invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5) resp = self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':invalid_date_in_future> ) self.assertEqual( resp.status_code,200) self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')
Такие же способы тестирования могут применяться для проверок других отображений.
Шаблоны
Django предоставляет API для тестирования, которое проверяет что функции отображения вызывают правильные шаблоны, а также позволяют убедиться, что им передаётся соответствующая информация. Кроме того, в Django имеется возможность использовать сторонние API для проверок того, что ваш HTML показывает то, что надо.
Другие рекомендованные инструменты для тестирования
Django фреймворк для тестирования помогает вам создавать эффективные юнит- и интеграционные тесты — мы рассмотрели только небольшую часть того, что может делать фреймворк unittest и совсем не упоминали дополнения Django (например, посмотрите на модуль unittest.mock, который подключает сторонние библиотеки тестирования).
Из всего множества сторонних инструментов тестирования, мы кратко опишем возможности двух:
- Coverage: Это инструмент Python, который формирует отчёты о том, какое количество кода выполняется во время проведения тестов. Это полезно для уточнения степени «покрытия» кода тестами.
- Selenium (en-US) это фреймворк проведения автоматического тестирования в настоящем браузере. Он позволяет вам имитировать взаимодействие пользователя с вашим сайтом (что является следующим шагом в проведении интеграционных тестов).
Домашняя работы
Существуют другие модели и отображения, которые мы могли бы протестировать. В качестве простого упражнения, попробуйте создать тестовый вариант для отображения AuthorCreate .
class AuthorCreate(PermissionRequiredMixin, CreateView): model = Author fields = '__all__' initial='date_of_death':'12/10/2016',> permission_required = 'catalog.can_mark_returned'
Помните, — вам надо проверить все, что касается вашего кода, или структуры. Это включает в себя: кто имеет доступ к отображению, начальную дату, применяемый шаблон, а также перенаправление из отображения в случае успеха.
Итоги
Написание тестов не является ни весельем, ни развлечением и, соответственно, при создании сайтов часто остаётся напоследок (или вообще не используется). Но тем не менее, они являются действенным механизмом, который позволяет вам убедиться, что ваш код в находится безопасности, даже если в него добавляются какие-либо изменения. Кроме того, тесты повышают эффективность поддержки вашего кода.
В данном руководстве мы продемонстрировали вам принципы написания тестов для ваших моделей, форм и отображений. Мы кратко перечислили что именно необходимо тестировать, что обычно сложно выявить в самом начале разработки. Существует много аспектов которые необходимо изучить, но даже с тем что мы уже узнали, вы имеете возможность создавать эффективные юнит-тесты для значительного улучшения процесса разработки.
Следующая и последняя часть руководства покажет вам как запустить ваш чудесный (и полностью протестированный!) веб-сайт Django.
Смотрите также
- Написание и запуск тестов (Django docs)
- Написание вашего первого приложения Django, часть 5 > Введение в автоматическое тестирование (Django docs)
- Инструменты для тестирования (Django docs)
- Продвинутое тестирование (Django docs)
- Путеводитель по тестированию в Django (Toast Driven Blog, 2011)
- Мастерская: Разработка через тесты с Django (TDD) (San Diego Python, 2014)
- Тестирование в Django (Часть 1) — Лучшие практики и Примеры (RealPython, 2013)
- Назад
- Обзор: Django
- Далее
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 3 авг. 2023 г. by MDN contributors.
Как покрыть приложение на Django модульными тестами
Примечание: чтобы использовать это руководство, вам понадобятся навыки, приобретенные после прочтения Как создать API с помощью Python и Django, а также код, над которым мы работали тогда. Мы продолжим работу с тем же API для приложения со списком дел, и будем использовать вот эту версию на GitHub.
Прошлым летом я работал над веб-приложением на Django со своими друзьями. Как-то раз я создал одну функцию и отправил своему другу на тестирование. Он ответил, что не смог ее протестировать, поскольку я случайно сломал функцию входа в систему. Однако это был не единственный случай, что-то подобное происходило каждую неделю. Основная проблема заключалась в том, что мы не использовали полноценное автоматизированное тестирование в нашем рабочем процессе.
Что такое автоматизированное тестирование?
Тестирование кода — одна из важнейших частей разработки. Тестирование может быть разным: можно протестировать приложение, всего лишь взглянув на веб-страницу, поиграв в видеоигру или проанализировав логи. Все зависит от типа вашего проекта. Ручное тестирование отнимает много времени и допускает возможность ошибок, поэтому профессиональные разработчики стараются уделять больше внимания автоматизированному тестированию. В этой статье мы рассмотрим общий вид автоматизированного тестирования, модульные тесты, а также поговорим о том, как автоматизированное тестирование может помочь нам в процессе разработки. Мы начнем с написания тестов для моделей и представлений (views) существующего API, затем, добавив новую функцию, попрактикуемся в разработке через тестирование.
Автоматизированное тестирование экономит время и делает программное обеспечение качественней. Поначалу ручное тестирование кажется быстрым: нужно просто запустить код и посмотреть, работает ли он. Однако со временем, чем больше и больше функций вы добавляете в свое приложение, тем больше времени начинает отнимать такой тип тестирования. К тому же вы можете просто забыть протестировать определенные вещи. Правильно реализованное автоматизированное тестирование охватывает все, работает всегда и занимает считанные секунды. Оно также делает код проще для понимания, что позволяет одновременно нескольким командам работать над кодом, не беспокоясь о том, что они могут сломать чью-то функцию.
Модульный тест по отдельности проверяет функциональность компонентов. Это тестирование самого низкого уровня, оно проверяет, что каждый компонент программы правильно работает в одиночку (интеграционные и системные тесты проверяют все компоненты вместе, а также их взаимодействия, но эта тема выходит за рамки данного руководства). При объектно-ориентированном подходе, например, вам пришлось бы писать модульные тесты для каждого объекта, а также для отдельных методов, в зависимости от их сложности. В Django мы используем модульное тестирование для каждой модели и представления.
Как в Django работает модульное тестирование?
Для работы с этим руководством, клонируйте вот этот проект из GitHub. Чтобы создать проект, выполните действия из первых трех параграфов в Как создать API с помощью Python и Django.
Когда вы используете python manage.py startapp appname для создания приложения на Django, один из создаваемых Django файлов папке appname имеет имя tests.py . Этот файл существует для размещения модульных тестов для моделей и других компонентов внутри приложения. По умолчанию этот файл содержит одну строчку кода: from django.test import TestCase . Test case содержит несколько связанных тестов для одного и того же фрагмента кода. TestCase — это объект Django, который мы будем наследовать для создания собственных модульных тестов. У класса есть два метода: setUp(self) и tearDown(self) , которые запускаются до и после отдельных тестовых функций для того, чтобы предоставить и очистить тестовую базу данных. Эта база независима от той базы данных, к которой вы получаете доступ с помощью python manage.py runserver . Чтобы взглянуть на код, откройте tests.py в папке todo нашего проекта.
class SigninTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') self.user.save() def tearDown(self): self.user.delete() def test_correct(self): user = authenticate(username='test', password='12test12') self.assertTrue((user is not None) and user.is_authenticated) def test_wrong_username(self): user = authenticate(username='wrong', password='12test12') self.assertFalse(user is not None and user.is_authenticated) def test_wrong_pssword(self): user = authenticate(username='test', password='wrong') self.assertFalse(user is not None and user.is_authenticated)
Здесь мы тестируем функцию входа в систему. Поскольку мы используем встроенные в Django методы, все должно работать нормально, если в базе данных нет никаких проблем. Нам нужны только простые тесты: аутентифицируйтесь, если предоставлены верные данные, и не делайте этого, если нет. Благодаря этому примеру мы можем увидеть кое-что еще в модульном тестировании в Django. Прежде всего, все тестовые методы в тестовом случае должны начинаться с test_ , чтобы быть выполненными при запуске тестовой команды python manage.py test . Остальные методы в тестовом случае нужно воспринимать как вспомогательные функции. Также необходимо знать, что все тестовые методы должны принимать self в качестве аргумента, где self является ссылкой на объект TestCase . Класс TestCase , который мы наследуем для создания нашего класса, содержит методы утверждений для проверки логических значений. Вызов self.assertSomething() проходит, если переданные в качестве аргументов значения соответствуют утверждению, в противном случае этого не происходит. Тестовый метод проходит, только если каждое утверждение в методе проходит.
class TaskTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') self.user.save() self.timestamp = date.today() self.task = Task(user=self.user, description='description', due=self.timestamp + timedelta(days=1)) self.task.save() def tearDown(self): self.user.delete() def test_read_task(self): self.assertEqual(self.task.user, self.user) self.assertEqual(self.task.description, 'description') self.assertEqual(self.task.due, self.timestamp + timedelta(days=1)) def test_update_task_description(self): self.task.description = 'new description' self.task.save() self.assertEqual(self.task.description, 'new description') def test_update_task_due(self): self.task.due = self.timestamp + timedelta(days=2) self.task.save() self.assertEqual(self.task.due, self.timestamp + timedelta(days=2))
Теперь давайте протестируем нашу модель: объект Task , определенный в models.py . Для тестового случая мы создаем пользователя и задачу (обратите внимание, что из-за того, что пользователь и задача связаны отношениями внешнего ключа, удаление пользователя в tearDown() приведет к удалению задачи). Здесь мы можем увидеть, что любой тестовый метод может иметь несколько утверждений и проходит только в том случае, если все они выполняются успешно. Когда мы обновляем задачу, мы можем записывать данные в базу вне функции setUp. В остальном, этот тест похож на тест функции входа. Большинство тестовых случаев для моделей представляют собой создание, чтение, модифицирование и удаление объектов в базе данных, хотя модели с методами и интереснее тестировать.
class SignInViewTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') def tearDown(self): self.user.delete() def test_correct(self): response = self.client.post('/signin/', 'username': 'test', 'password': '12test12'>) self.assertTrue(response.data['authenticated']) def test_wrong_username(self): response = self.client.post('/signin/', 'username': 'wrong', 'password': '12test12'>) self.assertFalse(response.data['authenticated']) def test_wrong_pssword(self): response = self.client.post('/signin/', 'username': 'test', 'password': 'wrong'>) self.assertFalse(response.data['authenticated'])
Тестировать представления несколько сложнее, чем модели. Однако поскольку мы пишем API, в отличие от веб-приложения, здесь можно не волноваться по поводу тестирования фронтенда. Большую часть ручных тестов посредством Postman можно заменить на тесты представлений. self.client — HTTP-клиент тестовой библиотеки Django. Мы используем его для создания post-запроса к «/signin/» с учетными данными пользователя. Мы тестируем то же, что и раньше: верные учетные данные, неправильное имя пользователя и неправильный пароль. Это очень полезно, так как мы видим, что если тесты модели не выявляют ошибок, а тесты представлений выявляют — проблема не в модели, что в свою очередь позволяет тратить меньше времени на устранение багов. Мы делаем примерно то же самое для представлений, связанных с задачами.
class AllTasksViewTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') self.user.save() self.timestamp = date.today() self.client.login(username='test', password='12test12') def tearDown(self): self.user.delete() def test_no_tasks(self): response = self.client.get('/all/') self.assertEqual(response.data, 'tasks': []>) def test_one_task(self): self.task1 = Task(user=self.user, description='description 1', due=self.timestamp + timedelta(days=1)) self.task1.save() response = self.client.get('/all/') self.assertEqual(response.data, 'tasks': [OrderedDict([('id', 1), ('description', 'description 1'), ('due', str(self.timestamp + timedelta(days=1)))])]>)
Этот случай тестирует конечную точку «/all/». На самом деле у этого теста больше методов, но фрагмент выше показывает только новое. Чтобы клиент мог действовать как вошедший в систему пользователь, в setUp мы используем self.client.login() . Затем мы создаем задачи и сравниваем их с ожидаемым отформатированным выводом. Этот пример хорошо иллюстрирует преимущества методов setUp() и tearDown() , так как задачи из одного теста не переносятся в другие. Опять же, этот тест изолирует компонент представления, поскольку базовая модель тестируется отдельно.
Когда разберетесь с тестовым кодом, запустите python manage.py test , чтобы выполнить все тесты. Давайте взглянем на результат:
Creating test database for alias 'default'. System check identified no issues (0 silenced). . FF. ====================================================================== FAIL: test_due_future (todo.tests.DueTodayTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/Philip/Code/WFH/mkdev_blog/djangotesting/taskmanager/todo/tests.py", line 155, in test_due_future self.assertFalse(self.task.due_today()) AssertionError: True is not false ====================================================================== FAIL: test_due_past (todo.tests.DueTodayTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/Philip/Code/WFH/mkdev_blog/djangotesting/taskmanager/todo/tests.py", line 161, in test_due_past self.assertFalse(self.task.due_today()) AssertionError: True is not false ---------------------------------------------------------------------- Ran 15 tests in 2.232s FAILED (failures=2) Destroying test database for alias 'default'.
Все тесты, не выявившие ошибок, помечаются . , а тесты, показавшие ошибки — F . Такие тесты также показывают, почему именно утверждения не прошли. Мы еще не говорили с вами о тех тестах, которые выявили ошибки, но мы исправимся чуть ниже. Вы могли заметить, что код теста очень подробен. Конечно же, мы протестировали лишь небольшую часть нашего функционала, но, несмотря на это, уже получили столько же строк кода, сколько и в файле представления. Этого и следовало ожидать. Если вы хотите, чтобы ваши тесты были точными, вы не можете проводить их слишком часто. При изменениях в коде некоторые тесты перестанут работать. Таким образом, вы сможете понять, какие ошибки и в каких тестах нужно будет устранить. Так что будьте готовы к тому, что код тестов будет все расти и расти, а в среднестатистическом приложении будет столько же строк тестового кода, сколько и у кода самого приложения.
Что такое разработка через тестирование?
Давайте на секундочку вернемся к рассказу из начала статьи. Наша команда неделя за неделей сражалась с багами и в результате написала модульные тесты для всей базы данных. Мы покрыли тестами абсолютно все, каждая строчка кода проверялась по меньшей мере одним тестом. Мы поработали так пару недель, пока не решили существенно изменить структуру нашей базы данных. Вместо того чтобы переписывать тесты, мы стали отбрасывать неработающие и буквально через несколько дней у нас стали снова вылезать случайные поломки. Разработка через тестирование помогла бы это предотвратить.
Чтобы тесты оставались актуальными, их нужно обновлять по мере обновления кода. Некоторые разработчики пользуются разработкой на основе тестов, чтобы всегда быть готовыми к любым изменениям в коде. Первое, что вы делаете при разработке функции — определяете что, собственно, эта функция будет делать. Разработка через тестирование формализует этот процесс, поскольку при таком подходе вы, прежде всего, прописываете тесты для этой самой функциональности. Основная идея заключается в том, что вы пишите один или несколько тестов, которые определяют функцию, переписываете код до тех пор, пока тесты не выявят ошибок, а затем снова пишите еще больше тестов. Вернемся к тестам, выявившим ошибки. Нам нужно написать метод due_today() в модели Task . Согласно тесту, этот метод должен возвращать True , если задача должна быть выполнена сегодня и False , если нет. Скопируйте код ниже для замены существующего метода due_today() в модели Task , а затем запустите тесты снова. python def due_today(self): return self.due == date.today()
Тест не показывает ошибок, что значит, что наша функция работает и можно продолжать. Подобный подход к разработке требует больших физических и умственных усилий поначалу для определения поведения кода, но в результате значительно упрощает сам процесс разработки.
Чтобы протестировать разобрались ли вы, попробуйте написать тесты для остальных представлений или используйте тесты для задания новых функций, а затем напишите эти функции. Одним из простых вариантов будет поле с логическим значением completed в модели task , которому может быть присвоено значение True как только задача будет выполнена. Это позволит нам не удалять выполненные задачи, а оставить их. Затем, подумайте о том, чтобы добавить тесты в ваши личные проекты. Да, вас может напугать перспектива покрытия тестами огромного проекта, который ранее не тестировался. Вместо того, чтобы пытаться протестировать все и сразу, попробуйте добавить тесты в маленькие фрагменты проекта или новые функции непосредственно во время разработки до полного покрытия.
Материалы для ознакомления:
- Официальное руководство Django, часть 5
- Обзор документации Django по тестированию
- Раздел по тестированию для продвинутых пользователей. Включает в себя покрытие тестами
- Документация Django по тестированию кода Django
© Copyright 2014 — 2024 mkdev | Privacy Policy