test_forms.py — Тестування форм¶
Два режими форми: bound vs unbound¶
Форма в Django — не просто HTML. Це валідація, нормалізація і security бар'єр.
# UNBOUND форма — без даних (для відображення порожньої форми)
form = NoteForm(user=self.alice)
# form.is_bound → False
# Корисно для тестування queryset полів (security)
# BOUND форма — з даними (для обробки POST запиту)
form = NoteForm(data={'title': 'Test', 'priority': 1}, user=self.alice)
# form.is_bound → True
# Можна викликати form.is_valid(), form.cleaned_data, form.errors
Як тестувати форму без HTTP¶
# Форму можна тестувати прямо, без client.post():
form = TagForm(data={'name': 'Python', 'color': '#ff0000'}, user=self.alice)
# Перевірка валідності:
form.is_valid() # True або False — перевіряємо
form.cleaned_data['name'] # 'python' — нормалізоване значення
form.errors # {'name': ['...']} — помилки по полях
# Перевіряємо валідну форму:
form = NoteForm(data={'title': 'Test', 'priority': 1}, user=self.alice)
self.assertTrue(form.is_valid(), msg=form.errors)
# ↑ msg=form.errors — якщо провалиться, побачимо ЧОМУ невалідна
# Перевіряємо невалідну форму:
form = NoteForm(data={'title': '', 'priority': 1}, user=self.alice)
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
# ↑ Перевіряємо що помилка у КОНКРЕТНОМУ полі (не загальна)
Структура test_forms.py¶
# notes_app/tests/test_forms.py
from django.contrib.auth.models import Group, User
from django.test import TestCase
from notes_app.forms import GroupCreateForm, NoteForm, TagForm
from notes_app.models import Notebook, Tag
class BaseFormTest(TestCase):
"""Базовий клас: alice і bob для security тестів."""
def setUp(self):
self.alice = User.objects.create_user('alice', password='pass123')
self.bob = User.objects.create_user('bob', password='pass123')
class TagFormTest(BaseFormTest):
"""Тести нормалізації і базової валідації TagForm."""
def test_name_normalized_to_lowercase(self):
"""Tag name зберігається у нижньому регістрі."""
form = TagForm(data={'name': 'PYTHON', 'color': '#3776AB'}, user=self.alice)
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['name'], 'python')
def test_name_strips_whitespace(self):
"""' work ' → 'work' (пробіли по краях прибираються)."""
form = TagForm(data={'name': ' work ', 'color': '#ff0000'}, user=self.alice)
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['name'], 'work')
def test_empty_name_is_invalid(self):
form = TagForm(data={'name': '', 'color': '#000000'}, user=self.alice)
self.assertFalse(form.is_valid())
self.assertIn('name', form.errors)
def test_name_too_long_is_invalid(self):
form = TagForm(data={'name': 'x' * 51, 'color': '#000000'}, user=self.alice)
self.assertFalse(form.is_valid())
NoteFormSecurityTest — повний security блок¶
class NoteFormSecurityTest(BaseFormTest):
"""
Найважливіший блок тестів форм — перевіряємо що queryset
фільтрує тільки об'єкти поточного юзера (захист від IDOR).
"""
def setUp(self):
super().setUp()
# Обидва юзери мають свої записники
self.alice_notebook = Notebook.objects.create(user=self.alice, title="Alice's")
self.bob_notebook = Notebook.objects.create(user=self.bob, title="Bob's")
# Alice має тег
self.alice_tag = Tag.objects.create(user=self.alice, name='my-tag')
self.bob_tag = Tag.objects.create(user=self.bob, name='bob-secret')
def test_notebook_queryset_contains_user_notebooks(self):
"""Alice бачить СВІЙ записник у dropdown."""
form = NoteForm(user=self.alice)
self.assertIn(self.alice_notebook, form.fields['notebook'].queryset)
def test_notebook_queryset_excludes_other_user_notebooks(self):
"""
Alice НЕ бачить записники Bob'а — IDOR неможливий.
БЕЗ захисту у формі:
NoteForm().fields['notebook'].queryset = Notebook.objects.all()
→ Alice бачить у dropdown Bob's Private Notebook!
→ Alice вибирає Bob's notebook → нотатка Alice записується до Bob
→ Bob бачить чужі нотатки у своєму записнику → DATA LEAK
З захистом (наш NoteForm.__init__):
queryset = Notebook.objects.filter(user=self.user)
→ Alice бачить тільки свої записники → IDOR неможливий
"""
form = NoteForm(user=self.alice)
self.assertNotIn(self.bob_notebook, form.fields['notebook'].queryset)
def test_tag_queryset_contains_user_tags(self):
"""Alice бачить свої теги у dropdown."""
form = NoteForm(user=self.alice)
self.assertIn(self.alice_tag, form.fields['tags'].queryset)
def test_tag_queryset_excludes_other_user_tags(self):
"""Alice НЕ бачить теги Bob'а."""
form = NoteForm(user=self.alice)
self.assertNotIn(self.bob_tag, form.fields['tags'].queryset)
def test_group_queryset_excludes_groups_user_not_in(self):
"""Alice не бачить групи де вона не є членом."""
group = Group.objects.create(name="Bob's Team")
self.bob.groups.add(group) # тільки Bob у групі
form = NoteForm(user=self.alice)
self.assertNotIn(group, form.fields['group'].queryset)
def test_form_without_user_has_empty_querysets(self):
"""
Якщо форму створити без user= (помилка розробника):
form = NoteForm(data=request.POST) ← забули user=request.user
НЕБЕЗПЕЧНО: queryset = Notebook.objects.all() → всі записники видимі
БЕЗПЕЧНО (наш код): queryset = Notebook.objects.none() → порожній список
Краще показати юзеру порожній dropdown ніж злити всі дані.
"""
form = NoteForm() # user не переданий
self.assertFalse(form.fields['notebook'].queryset.exists())
self.assertFalse(form.fields['tags'].queryset.exists())
Перевірка cleaned_data (нормалізація)¶
class NoteFormValidationTest(BaseFormTest):
def test_valid_form_with_required_fields(self):
"""Мінімально необхідні поля → форма валідна."""
form = NoteForm(
data={'title': 'My Note', 'priority': 1},
user=self.alice,
)
self.assertTrue(form.is_valid(), msg=form.errors)
def test_empty_title_is_invalid(self):
"""Порожній title → ValidationError для поля title."""
form = NoteForm(
data={'title': '', 'priority': 1},
user=self.alice,
)
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_priority_valid_range(self):
"""Priority від 1 до 4 — валідний."""
for p in [1, 2, 3, 4]:
form = NoteForm(data={'title': 'Test', 'priority': p}, user=self.alice)
self.assertTrue(form.is_valid(), msg=f'priority={p} should be valid')
Перевірка помилок у конкретному полі¶
def test_specific_field_error(self):
"""Перевіряємо що помилка у КОНКРЕТНОМУ полі, а не загальна."""
form = NoteForm(data={'title': '', 'priority': 99}, user=self.alice)
self.assertFalse(form.is_valid())
# Перевіряємо конкретне поле:
self.assertIn('title', form.errors) # title: порожнє → required
# або:
self.assertIn('priority', form.errors) # priority: 99 → out of range
# НЕ перевіряємо тільки form.errors (можуть бути помилки у всіх полях)
# Перевіряємо що саме у потрібному полі
Таблиця: що перевіряємо у кожній формі¶
| Форма | Що тестуємо |
|---|---|
TagForm |
clean_name: lowercase + strip, порожнє ім'я → помилка |
NoteForm |
queryset для notebook/tags/group фільтрується по user |
NoteForm |
без user= → порожні queryset (безпечний fallback) |
NoteForm |
порожній title → invalid, пріоритет 1–4 → valid |
GroupCreateForm |
clean_name: дублікат → ValidationError, strip whitespace |
Запуск тестів форм¶
# crispy_notes_project:
python manage.py test hello_app.tests.test_forms -v 2
# notes_chat_app (Docker):
docker compose run --rm web python manage.py test notes_app.tests.test_forms -v 2
# Конкретний клас:
docker compose run --rm web python manage.py test \
notes_app.tests.test_forms.NoteFormSecurityTest -v 2
Далі¶
- test_views.py — Test Client, force_login, три паттерни, схема доступу