**Зачем отделять тесты от производственного кода?**
Тесты не являются частью вашего продукта. Они живут рядом с кодом, чтобы верифицировать его, но пользователи никогда их не запускают. Хранение тестов в выделенном каталоге `tests/` (или в файлах `test_*.py`) делает это разделение явным, избегает засорения импортов и даёт контроль над тем, что попадёт в сборку для деплоя.
**Как pytest обнаруживает тесты:**
pytest обходит дерево каталогов проекта и применяет следующие правила на каждом уровне:
1. **Уровень файлов** -- собирать файлы, соответствующие `test_*.py` или `*_test.py`
2. **Уровень модуля** -- собирать функции, имя которых начинается с `test_`
3. **Уровень классов** -- собирать классы, имя которых начинается с `Test` (без `__init__`)
4. **Уровень методов** -- собирать методы внутри классов `Test*`, начинающиеся с `test_`
Несколько примеров того, что собирается и что пропускается:
```
test_math.py -> собирается
math_test.py -> собирается
math.py -> не собирается
helpers.py -> не собирается
def test_add(): -> собирается
def add_test(): -> не собирается (нет префикса test_)
def check_add(): -> не собирается
class TestMath: -> собирается
class MathTest: -> не собирается (неверный префикс)
class TestMath:
def test_add(): -> собирается
def add_check(): -> не собирается
```
Запустите `pytest --collect-only`, чтобы увидеть, что именно будет собрано, прежде чем что-то запускать.
**Обычные функции vs тестовые классы:**
Обычные функции проще и достаточны для большинства случаев:
```python
def test_add():
assert add(2, 3) == 5
def test_subtract():
assert subtract(5, 3) == 2
```
Используйте классы, когда у вас есть группа связанных тестов, объединённых одной концепцией -- классы работают как пространство имён и делают связь явной:
```python
class TestUserAccount:
def test_creation(self):
...
def test_deactivation(self):
...
def test_password_change(self):
...
```
Классы также поддерживают `setup_method` / `teardown_method` для инициализации перед каждым тестом. Однако для совместно используемых *данных* фикстуры (рассматриваются далее) являются более чистым решением, чем состояние на уровне класса.
**Рекомендуемая структура проекта:**
```
my_project/
├── src/
│ ├── users.py
│ └── products.py
├── tests/
│ ├── conftest.py <- общие фикстуры (рассматриваются в следующей теме)
│ ├── test_users.py
│ └── test_products.py
└── pytest.ini
```
Отражение структуры `src/` внутри `tests/` позволяет легко найти тесты для любого модуля.
Обнаружение в действии: функции, классы и структура
В файле ниже есть 6 плоских тест-функций для двух тем (разворот строк и фильтрация списков). Реорганизуйте их в два класса `Test*` -- `TestReverse` и `TestFilter` -- не изменяя ни одного оператора assert.
Создайте класс `TestStringUtils` с тремя тест-методами: один для `capitalize_words(s)` (делает первую букву каждого слова заглавной), один для `count_vowels(s)` (считает a, e, i, o, u без учёта регистра) и один для `is_palindrome(s)`.
def capitalize_words(s):
return ' '.join(word.capitalize() for word in s.split())
def count_vowels(s):
return sum(1 for c in s.lower() if c in 'aeiou')
def is_palindrome(s):
return s == s[::-1]
class TestStringUtils:
pass # добавьте три тест-метода здесь
Решение
def capitalize_words(s):
return ' '.join(word.capitalize() for word in s.split())
def count_vowels(s):
return sum(1 for c in s.lower() if c in 'aeiou')
def is_palindrome(s):
return s == s[::-1]
class TestStringUtils:
def test_capitalize_words(self):
assert capitalize_words('hello world') == 'Hello World'
assert capitalize_words('python') == 'Python'
def test_count_vowels(self):
assert count_vowels('hello') == 2
assert count_vowels('rhythm') == 0
assert count_vowels('AEIOU') == 5
def test_is_palindrome(self):
assert is_palindrome('racecar') is True
assert is_palindrome('hello') is False
assert is_palindrome('') is True
Добавьте `setup_method` в класс `TestShoppingCart`, чтобы каждый тест получал свежую корзину. Корзина должна быть словарём `{'items': [], 'total': 0}`. Убедитесь, что `test_cart_still_empty_after_other_test` проходит даже после `test_can_add_item`.
class TestShoppingCart:
# добавьте setup_method здесь
def test_cart_starts_empty(self):
assert self.cart['items'] == []
def test_total_starts_at_zero(self):
assert self.cart['total'] == 0
def test_can_add_item(self):
self.cart['items'].append('apple')
assert len(self.cart['items']) == 1
def test_cart_still_empty_after_other_test(self):
# должен проходить даже если test_can_add_item выполнился первым
assert self.cart['items'] == []
Создайте два тестовых файла: `test_math.py` с тестами для `add` и `multiply`, и `test_strings.py` с тестами для `str.upper()` и `str.lower()`. Запустите `pytest -k 'math'` и убедитесь, что выполняются только тесты для математических операций. Затем запустите `pytest -k 'multiply'`.
# test_math.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
# напишите test_add и test_multiply здесь
# test_strings.py
# напишите test_upper и test_lower
Создайте минимальную структуру проекта: поместите `string_utils.py` с функцией `slugify(s)` (переводит в нижний регистр и заменяет пробелы на дефисы) в `src/`, поместите два теста в `tests/test_string_utils.py` и добавьте `pytest.ini` с `testpaths = tests`. Запустите pytest из корня проекта.
# src/string_utils.py
def slugify(s):
pass # нижний регистр + замена пробелов дефисами
# tests/test_string_utils.py
# импортируйте и протестируйте slugify здесь
# pytest.ini
# [pytest]
# testpaths = tests
Мы используем необходимые cookies для работы сайта. С вашего разрешения мы также можем сохранять настройки сайта и использовать аналитические и рекламные cookies, чтобы понимать использование сайта и поддерживать развитие проекта.
* Вы всегда можете изменить свой выбор в настройках сайта.
Выберите категории cookies
Настройки аналитики
Можно отключить аналитику использования платформы. Также можно отправить в Google Analytics запрос на удаление данных об использовании этого сайта, связанных с этим браузером.
Учебный workspace
Учитесь, читая, запуская код и решая задачи.
Практикуйте программирование с пояснениями тем, упражнениями, инструментами browser IDE, проверкой regex и тренировкой печати кода в одном workspace.
Открывайте инструменты во вкладках.Упражнения, IDE-инструменты и тренажеры остаются доступными как вкладки сайта.
Переключайтесь без потери контекста.Переходите между пояснениями, кодом и утилитами, сохраняя свое место.
Используйте sidebar как карту.Левые панели содержат навигацию, настройки, файлы, libraries и управление инструментами.
PythonJavaScriptSQLite
Одна IDE, три практичных режима
Python в браузере.Запускайте небольшие скрипты, пробуйте библиотеки и тренируйте API-запросы без установки.
JavaScript для быстрых экспериментов.Проверяйте код для браузера и сравнивайте идеи рядом с учебными материалами.
SQLite для практики с данными.Открывайте обозреватель базы данных, изучайте таблицы, пишите запросы и учитесь SQL локально.
ТемаIDE
Работайте рядом в split tabs
Держите инструкции перед глазами.Откройте упражнение или справочную страницу рядом с IDE, вместо постоянных переключений.
Сравнивайте инструменты во время обучения.Размещайте проверки regex, пояснения и эксперименты с кодом рядом, когда это нужно для задачи.
Закройте split, когда закончите.Workspace вернётся к одной сфокусированной вкладке, а открытые вкладки сайта останутся доступны.
Тренажер слепой печати кода
Или просто текста
Тренажер рассчитан на физическую клавиатуру.Откройте этот раздел на ноутбуке или компьютере с широким экраном. На телефоне тренировка слепой печати не будет корректной.
Скорость: 0 зн/мин
0 слов/мин
Лучшая скорость (60с): 0 зн/мин
0 слов/мин
Ошибки: 0
Общее время: 0.0 с
Для активации режима слепого набора не подсматривайте на физическую клавиатуру.