При тестировании форм общий подход остаётся прежним: не стоит тратить время на проверку Python, Django, подключаемых библиотек и приложений. Можно принять за аксиому, что поле для картинки примет только картинку, а поле электронной почты — только почтовый адрес: разработчики фреймворка уже протестировали это.
Тестировать следует лишь собственноручно написанный код.
Тестирование форм
Все тесты для форм будет удобно сложить в отдельный файл:
└── my_app
├── __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
В проекте Todo форма создана на основе модели:
# deals/forms.py
from django.core.exceptions import ValidationError
from django import forms
from pytils.translit import slugify
from .models import Task
class TaskCreateForm(forms.ModelForm):
"""Форма для создания задачи."""
class Meta:
model = Task
# Магия Джанго: через '__all__' создаётся кортеж из всех полей модели;
# labels и help_texts берутся из полей модели
fields = '__all__'
# Валидация поля slug
def clean_slug(self):
"""Обрабатывает случай, если slug не уникален."""
cleaned_data = super().clean()
slug = cleaned_data.get('slug')
if not slug:
title = cleaned_data.get('title')
slug = slugify(title)[:100]
if Task.objects.filter(slug=slug).exists():
raise ValidationError(
f'Адрес "{slug}" уже существует, '
'придумайте уникальное значение'
)
return slug
Тест стандартного поведения формы
Обычно форма работает по такому сценарию:
- Пользователь заполняет форму и отправляет её; данные из формы уходят POST-запросом на сервер.
- Данные проходят валидацию на сервере.
- Если данные валидны — выполняется какое-то полезное действие: данные записываются в базу, или отправляется письмо, или пользователь, отправивший данные, авторизуется на сайте.В проекте Todo заполнение формы приводит к созданию новой записи в модели
Task
. - После отправки формы пользователь редиректится на страницу с сообщением об успешной отправке формы.
Имеет смысл протестировать, что
- после успешной валидации в модели Task появится новая запись;
- после отправки валидной формы происходит переадресация на страницу
/added/
Сохранение картинки можно не тестировать: это делает Django, в этом ему можно доверять.
# deals/tests/tests_form.py
import shutil
import tempfile
from deals.forms import TaskCreateForm
from deals.models import Task
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, TestCase, override_settings
from django.urls import reverse
# Создаем временную папку для медиа-файлов;
# на момент теста медиа папка будет переопределена
TEMP_MEDIA_ROOT = tempfile.mkdtemp(dir=settings.BASE_DIR)
# Для сохранения media-файлов в тестах будет использоваться
# временная папка TEMP_MEDIA_ROOT, а потом мы ее удалим
@override_settings(MEDIA_ROOT=TEMP_MEDIA_ROOT)
class TaskCreateFormTests(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Создаем запись в базе данных для проверки сушествующего slug
Task.objects.create(
title='Тестовый заголовок',
text='Тестовый текст',
slug='first'
)
# Создаем форму, если нужна проверка атрибутов
cls.form = TaskCreateForm()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
# Модуль shutil - библиотека Python с удобными инструментами
# для управления файлами и директориями:
# создание, удаление, копирование, перемещение, изменение папок и файлов
# Метод shutil.rmtree удаляет директорию и всё её содержимое
shutil.rmtree(TEMP_MEDIA_ROOT, ignore_errors=True)
def setUp(self):
# Создаем неавторизованный клиент
self.guest_client = Client()
def test_create_task(self):
"""Валидная форма создает запись в Task."""
# Подсчитаем количество записей в Task
tasks_count = Task.objects.count()
# Для тестирования загрузки изображений
# берём байт-последовательность картинки,
# состоящей из двух пикселей: белого и чёрного
small_gif = (
b'\x47\x49\x46\x38\x39\x61\x02\x00'
b'\x01\x00\x80\x00\x00\x00\x00\x00'
b'\xFF\xFF\xFF\x21\xF9\x04\x00\x00'
b'\x00\x00\x00\x2C\x00\x00\x00\x00'
b'\x02\x00\x01\x00\x00\x02\x02\x0C'
b'\x0A\x00\x3B'
)
uploaded = SimpleUploadedFile(
name='small.gif',
content=small_gif,
content_type='image/gif'
)
form_data = {
'title': 'Тестовый заголовок',
'text': 'Тестовый текст',
'image': uploaded,
}
# Отправляем POST-запрос
response = self.guest_client.post(
reverse('deals:home'),
data=form_data,
follow=True
)
# Проверяем, сработал ли редирект
self.assertRedirects(response, reverse('deals:task_added'))
# Проверяем, увеличилось ли число постов
self.assertEqual(Task.objects.count(), tasks_count+1)
# Проверяем, что создалась запись с заданным слагом
self.assertTrue(
Task.objects.filter(
slug='testovyij-zagolovok',
text='Тестовый текст',
image='tasks/small.gif'
).exists()
)
При подготовке данных для тестирования не стоит применять картинки, документы или какие-то иные файлы с вашего компьютера. Все данные для тестов должны быть в фикстурах.
Самый простой подход — использовать байт-строки (неизменяемые последовательности отдельных байтов) и эмулировать файл картинки с помощью встроенной в Django библиотеки SimpleUploadedFile, как это и сделано в листинге.
При тестировании модуль SimpleUploadFile
не эмулирует сохранение картинок, а на самом деле сохраняет их в директорию media/tasks. В результате после каждого теста в этой директории будет добавляться по картинке. Лучше прибраться за собой и не оставлять никакого мусора после тестов.
При запуске тестов можно создавать временную директорию для сохранения файлов. Эта операция описана в методе setUpClass
.
Метод tearDownClass
выполняется по окончании всех тестов класса, и в нём через метод shutil.rmtree()
временная папка удаляется — вместе с загруженными в неё файлами. Теперь всё чистенько, как будто ничего и не было.
Тест дополнительных сценариев формы
Запись в поле slug
должна быть уникальной, однако при заполнении поля (ручном или автоматическом) может быть создано неуникальное значение.
В такой ситуации запись в базе данных не должна создаваться, но сайт не должен упасть.
# deals/tests/tests_form.py
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, TestCase
from django.urls import reverse
from deals.forms import TaskCreateForm
from deals.models import Task
class TaskCreateFormTests(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Создаем запись в базе данных
Task.objects.create(
title='Тестовый заголовок',
text='Тестовый текст',
slug='first'
)
def setUp(self):
# Создаем неавторизованный клиент
self.guest_client = Client()
def test_cant_create_existing_slug(self):
# Подсчитаем количество записей в Task
tasks_count = Task.objects.count()
form_data = {
'title': 'Заголовок из формы',
'text': 'Текст из формы',
'slug': 'first', # Отправим в форму slug, который уже есть в БД
}
response = self.guest_client.post(
reverse('deals:home'),
data=form_data,
follow=True
)
# Убедимся, что запись в базе данных не создалась:
# сравним количество записей в Task до и после отправки формы
self.assertEqual(Task.objects.count(), tasks_count)
# Проверим, что форма вернула ошибку с ожидаемым текстом:
# из объекта responce берём словарь 'form',
# указываем ожидаемую ошибку для поля 'slug' этого словаря
self.assertFormError(
response,
'form',
'slug',
'Адрес "first" уже существует, придумайте уникальное значение'
)
# Проверим, что ничего не упало и страница отдаёт код 200
self.assertEqual(response.status_code, 200)
В тесте применён метод assertFormError()
. Его назначение — проверить, возвращают ли поля формы ожидаемые ошибки.В аргументах метода, кроме объекта response
, указывается
- имя формы (как оно указано в словаре
context
), - имя поля, ошибку которого нужно протестировать,
- ожидаемый текст ошибки.
Официальное описание метода assertFormError()
можно посмотреть в документации.
Специальная глава для разработчиков с coverage-зависмостью
Так удачно сложилось, что метки полей формы (labels) и тексты подсказок (help_text) уже протестированы в моделях, второй раз их тестировать не надо. В 99 из 100 проектов их вообще не тестируют.
Хозяйке на заметку — labels и help_texts лучше определять и тестировать в моделях, а не в формах: это более универсальный способ.Но если labels и help_texts были переопределены и это важно для проекта при генерации форм — их следует проверить.Например, в каком-то воображаемом файле forms.py форма описана так:
# Воображаемый my_app/forms.py
from django import forms
from .models import Imagination
class ImaginForm(forms.ModelForm):
class Meta:
model = Imagination
fields = '__all__'
labels = {
'title': 'Заголовок из формы',
}
help_texts = {
'help_texts': 'Текст подсказки из формы',
}
Проверить labels и help_text этой формы можно так:
# Воображаемый my_app/tests/test_forms.py
from django.test import Client, TestCase
from my_app.forms import ImaginForm
class ImaginFormTests(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.form = ImaginForm()
def test_title_label(self):
title_label = ImaginFormTests.form.fields['title'].label
self.assertTrue(title_label, 'Заголовок из формы')
def test_title_help_text(self):
title_help_text = ImaginFormTests.form.fields['title'].help_text
self.assertTrue(title_help_text, 'Текст подсказки из формы')