pytest: короткі тести, fixtures і pytest-django¶
Після цього файлу ти зможеш писати тести через
pytest, запускати їх, використовувати fixtures, parametrization і підключати pytest до Django-проєкту.
1. Навіщо це потрібно¶
pytest часто вибирають, бо він дозволяє писати тести простіше:
Без класу, без self.assertEqual, без зайвого шаблону.
Але простіший синтаксис не означає “менше думати”. Хороший pytest-тест так само має чітке очікування, зрозумілу назву і нормальні тестові дані.
Встановлення:
Запуск:
2. Ментальна модель¶
pytest — це test runner, який дуже добре вміє знаходити тести і підкладати їм потрібні дані.
Уяви, що тест каже:
pytest бачить параметр user_data і питає:
“У мене є fixture з такою назвою?”
Якщо є — запускає fixture і передає результат у тест.
3. Перший приклад з нуля¶
Структура:
math_utils.py:
test_math_utils.py:
from math_utils import add, is_even
def test_add_two_numbers():
result = add(2, 3)
assert result == 5
def test_four_is_even():
assert is_even(4)
def test_five_is_not_even():
assert not is_even(5)
Запуск:
Результат:
4. Як pytest знаходить тести¶
Типові правила:
| Що | Правило | Приклад |
|---|---|---|
| Файл | test_*.py або *_test.py |
test_math_utils.py |
| Функція | test_* |
test_add_two_numbers |
| Клас | Test* без __init__ |
class TestCart: |
pytest також може запускати unittest-тести, але для навчання краще не змішувати стилі в одному файлі без потреби.
5. Простий assert¶
У pytest використовують звичайний Python assert.
Якщо тест падає, pytest показує деталі.
Приклад:
Це зручно: видно реальний результат і очікуване значення.
Порівняння:
| unittest | pytest |
|---|---|
self.assertEqual(result, 5) |
assert result == 5 |
self.assertTrue(user.is_active) |
assert user.is_active |
self.assertFalse(form.is_valid()) |
assert not form.is_valid() |
self.assertIn("title", errors) |
assert "title" in errors |
6. Fixtures: тестові дані без дублювання¶
Проблема:
def test_user_has_email():
user = {"username": "student", "email": "student@example.com"}
assert user["email"] == "student@example.com"
def test_user_has_username():
user = {"username": "student", "email": "student@example.com"}
assert user["username"] == "student"
Дані дублюються.
Рішення:
import pytest
@pytest.fixture
def user_data():
return {
"username": "student",
"email": "student@example.com",
}
def test_user_has_email(user_data):
assert user_data["email"] == "student@example.com"
def test_user_has_username(user_data):
assert user_data["username"] == "student"
Що відбувається:
- pytest бачить параметр
user_data; - шукає fixture з назвою
user_data; - запускає fixture;
- передає результат у тест.
7. conftest.py: спільні fixtures¶
Якщо fixture потрібна багатьом файлам, її часто кладуть у conftest.py.
conftest.py:
test_notes.py:
Не треба імпортувати note_data. pytest сам знайде fixture.
8. Parametrization: багато сценаріїв без копіпасти¶
Погано:
def test_empty_title_is_invalid():
...
def test_empty_content_is_invalid():
...
def test_long_title_is_invalid():
...
Краще:
import pytest
@pytest.mark.parametrize(
"title, content",
[
("", "Text"),
("Valid title", ""),
("x" * 256, "Text"),
],
)
def test_note_form_invalid_data(title, content):
form = NoteForm(data={"title": title, "content": content})
assert not form.is_valid()
pytest запустить цей тест тричі з різними даними.
Коли parametrization корисна:
| Ситуація | Приклад |
|---|---|
| Кілька invalid inputs | "", None, дуже довгий рядок |
| Кілька очікуваних результатів | slug generation |
| Перевірка edge cases | 0, -1, max value |
9. pytest-django¶
Для Django потрібен plugin:
Зазвичай додають файл pytest.ini:
[pytest]
DJANGO_SETTINGS_MODULE = notes_chat_app.settings
python_files = tests.py test_*.py *_tests.py
Тест з базою:
import pytest
from notes.models import Note
@pytest.mark.django_db
def test_note_str_returns_title():
note = Note.objects.create(title="Pytest note", content="Hello")
assert str(note) == "Pytest note"
Без @pytest.mark.django_db pytest не дозволить звертатися до бази. Це не примха. Це захист: тест має явно сказати, що йому потрібна база.
10. pytest чи unittest¶
| Питання | unittest | pytest |
|---|---|---|
| Чи треба встановлювати | Ні | Так |
| Стиль | Класи й methods | Функції, fixtures |
| Assertions | self.assertEqual |
assert |
| Django за замовчуванням | Так | Через pytest-django |
| Зручно для | Django built-in style | Швидких тестів, fixtures, parametrization |
Висновок:
- якщо курс або команда використовує Django default — вчи
unittest/TestCase; - якщо хочеш коротший синтаксис і гнучкі fixtures — вчи
pytest; - у реальних проєктах можна зустріти обидва стилі.
11. Mermaid-схема¶
flowchart TD
A["pytest"] --> B["Знаходить test_*.py"]
B --> C["Знаходить test_* функції"]
C --> D{"Тест має параметри?"}
D -->|Так| E["Шукає fixtures"]
D -->|Ні| F["Запускає тест"]
E --> F
F --> G["Виконує assert"]
G --> H{"Очікування збіглось?"}
H -->|Так| I["passed"]
H -->|Ні| J["failed з поясненням"]
12. Типові помилки початківців¶
| Помилка | Чому виникає | Як виправити |
|---|---|---|
| Fixture називається не так, як параметр | pytest шукає fixture за назвою | Узгодь назви |
Забули @pytest.mark.django_db |
Тест лізе в БД без дозволу | Додай marker |
| Parametrized test став нечитабельним | Забагато даних в одному місці | Розбий або дай зрозумілі ids |
| Fixture робить усе одразу | Хочеться мати “готовий світ” | Роби маленькі fixtures |
| Змішали unittest і pytest без потреби | Копіювали з різних джерел | Обери стиль для одного файлу |
13. Практика¶
- Створи
math_utils.pyз функцієюis_even(number). - Напиши pytest-тести для
2,3,0,-4. - Перепиши ці тести через
@pytest.mark.parametrize. - Створи fixture
note_data. - Якщо маєш Django-проєкт, встанови
pytest-djangoі напиши тест дляNote.__str__.
Приклад parametrization:
@pytest.mark.parametrize(
"number, expected",
[
(2, True),
(3, False),
(0, True),
(-4, True),
],
)
def test_is_even(number, expected):
assert is_even(number) is expected
14. Питання для самоперевірки¶
- Чим pytest відрізняється від unittest у стилі написання?
- Як pytest знаходить fixtures?
- Для чого потрібен
conftest.py? - Коли parametrization краща за кілька окремих тестів?
- Навіщо потрібен
@pytest.mark.django_db?