## Проблема дублирования фикстур
По мере роста тест-сьюта на нескольких файлах возникает желание использовать одни и те же фикстуры в разных местах -- `sample_user`, соединение с базой данных, токен авторизации. Дублировать их -- дополнительная нагрузка на сопровождение; импортировать из другого тестового файла неудобно и нарушает модель обнаружения pytest.
`conftest.py` решает это чисто. Это специальный файл, который pytest обнаруживает автоматически. Любая фикстура, определённая в нём, доступна всем тестам в той же директории и всех поддиректориях -- **без какого-либо импорта**.
## Как pytest обнаруживает conftest.py
Когда pytest собирает тест из `tests/unit/test_users.py`, он загружает каждый `conftest.py`, найденный от корневой директории до директории тестового файла:
```
project/
+-- conftest.py <- загружается 1-м (фикстуры уровня проекта)
+-- tests/
+-- conftest.py <- загружается 2-м (фикстуры тест-сьюта)
+-- unit/
+-- conftest.py <- загружается 3-м (фикстуры юнит-тестов)
+-- test_users.py <- тестовый файл
```
Фикстуры каждого уровня доступны всем тестам ниже этого уровня. Тестовый файл никогда не импортирует из conftest -- pytest автоматически внедряет фикстуры по имени параметра.
## Несколько conftest-файлов и приоритет переопределения
Если одно и то же имя фикстуры встречается в нескольких conftest-файлах, побеждает **ближайший** -- тот, что ближе к тестовому файлу. Это позволяет задать разумное значение по умолчанию в корне и переопределить его для конкретного поддерева:
```
project/
+-- conftest.py # base_url = 'https://prod.example.com'
+-- tests/
+-- conftest.py # base_url = 'https://staging.example.com' <- переопределяет для tests/
+-- local/
+-- conftest.py # base_url = 'http://localhost:8000' <- переопределяет для local/
```
Тесты в `tests/local/` используют `localhost`; всё остальное в `tests/` использует staging.
## Scope фикстур в conftest
Scope работает так же, как и в фикстурах внутри файла, но фикстуры с session scope в conftest особенно мощны -- они выполняются один раз для всей pytest-сессии:
```python
# conftest.py
import pytest
import json
@pytest.fixture(scope='session')
def product_catalog():
with open('tests/fixtures/products.json') as f:
return json.load(f) # загружается один раз; используется каждым нуждающимся тестом
```
Это правильное место для дорогостоящей инициализации: аутентифицированные HTTP-сессии, соединения с БД, загрузка больших наборов данных. Выполнить один раз, разделить результат.
## Продолжительность жизни scope
| Scope | Создаётся | Уничтожается | Лучше всего для |
|-------|---------|-----------|---------|
| `function` (по умолчанию) | перед каждым тестом | после каждого теста | изолированное состояние на тест |
| `class` | перед первым тестом в классе | после последнего теста в классе | общая инициализация на уровне класса |
| `module` | перед первым тестом в файле | после последнего теста в файле | ресурсы на уровне файла |
| `session` | перед первым тестом запуска | после последнего теста запуска | дорогостоящая глобальная инициализация |
## conftest -- не модуль для импорта
Никогда не пишите `from conftest import my_fixture`. pytest обрабатывает обнаружение и внедрение автоматически. Импорт из conftest обходит этот механизм и создаёт скрытые баги.
## Фикстуры с yield для завершения
`yield` в фикстуре разделяет инициализацию и завершение. Код после `yield` выполняется, когда заканчивается scope фикстуры -- даже если тест упал:
```python
@pytest.fixture(scope='session')
def db_connection():
conn = open_connection()
yield conn # здесь выполняется тестовый код; conn внедряется в тест
conn.close() # выполняется после последнего теста, использующего эту фикстуру
```
**Обнаружение:** pytest загружает `conftest.py` от корневой директории до директории тестового файла. Импорт не нужен -- фикстуры внедряются по имени параметра.
**Приоритет переопределения:** победит ближайший conftest. `tests/unit/conftest.py` переопределяет `tests/conftest.py` для тестов внутри `tests/unit/`.
**Продолжительность жизни scope фикстуры:**
| Scope | `setup` запускается | `teardown` запускается |
|-------|-------------|----------------|
| `function` | перед каждым тестом | после каждого теста |
| `class` | перед первым методом | после последнего метода |
| `module` | перед первым тестом в файле | после последнего теста в файле |
| `session` | один раз при запуске `pytest` | в самом конце |
**Фикстура с yield (инициализация + завершение):**
```python
@pytest.fixture(scope='session')
def resource():
obj = setup()
yield obj # тесты получают obj
teardown(obj) # выполняется когда scope заканчивается
```
**Фикстура, зависящая от другой фикстуры:**
```python
@pytest.fixture
def auth_headers(auth_token): # auth_token -- другая фикстура
return {'Authorization': f'Token {auth_token}'}
```
**Косвенная параметризация:**
```python
@pytest.fixture
def user(request):
return create_user(role=request.param)
@pytest.mark.parametrize('user', ['admin', 'viewer'], indirect=True)
def test_access(user): ...
```
**Никогда** не пишите `from conftest import fixture` -- pytest обрабатывает это автоматически.
Создайте `conftest.py` в корне проекта с фикстурой `sample_users`, возвращающей список из трёх словарей пользователей (каждый с ключами `"name"` и `"email"`). Создайте `test_users_a.py` с тестом, проверяющим `len(sample_users) == 3`, и `test_users_b.py` с тестом, проверяющим, что каждый словарь содержит ключ `"email"`. Ни один тестовый файл не должен импортировать что-либо из `conftest.py`. Запустите `pytest -v` и убедитесь, что оба теста проходят.
# conftest.py
import pytest
@pytest.fixture
def sample_users():
# вернуть список из 3 словарей, каждый с 'name' и 'email'
pass
# test_users_a.py
def test_users_count(sample_users):
# проверить len(sample_users) == 3
pass
# test_users_b.py
def test_users_have_email(sample_users):
# проверить 'email' в каждом пользователе
pass
Решение
# conftest.py
import pytest
@pytest.fixture
def sample_users():
return [
{'name': 'Alice', 'email': '[email protected]'},
{'name': 'Bob', 'email': '[email protected]'},
{'name': 'Carol', 'email': '[email protected]'},
]
# test_users_a.py
def test_users_count(sample_users):
assert len(sample_users) == 3
# test_users_b.py
def test_users_have_email(sample_users):
for user in sample_users:
assert 'email' in user
02
Фикстура с session scope, загружающая файл один раз
Создайте `settings.json` с содержимым `{"debug": true, "page_size": 10}`. Создайте `conftest.py` с фикстурой `config` с `session` scope, которая читает этот файл и выводит `"Loading config"` перед возвратом распарсенного словаря. Напишите две тест-функции в двух отдельных файлах -- обе используют `config`. Запустите `pytest -v -s` и убедитесь, что `"Loading config"` появляется в выводе ровно один раз, а не дважды.
Создайте корневой `conftest.py` с фикстурой `base_url`, возвращающей `"https://apilearn.tukas.dev"`. Создайте поддиректорию `local/` с собственным `conftest.py`, переопределяющим `base_url` на `"http://localhost:8000"`. Напишите `test_root.py` в корне и `local/test_local.py` внутри поддиректории -- каждый должен выводить `base_url` с флагом `-s`. Запустите `pytest -v -s` и убедитесь, что в каждом тесте появляется правильный URL.
Создайте `conftest.py` с фикстурой `scaled`, которая читает `request.param` (число) и возвращает `request.param * 10`. Напишите параметризованный тест с `indirect=True` и параметрами `[2, 5, 7]`. Каждый запуск теста должен получать `20`, `50` и `70` соответственно -- проверьте это. Запустите `pytest -v` и убедитесь, что в выводе появляются три отдельных тест-кейса.
# conftest.py
import pytest
@pytest.fixture
def scaled(request):
# вернуть request.param умноженный на 10
pass
# test_scale.py
import pytest
@pytest.mark.parametrize('scaled', [2, 5, 7], indirect=True)
def test_scaled_value(scaled):
# проверить, что scaled равен 20, 50 или 70 в зависимости от переданного параметра
pass
Создайте `conftest.py` с фикстурой `temp_file`, которая: (1) создаёт `Path("test_output.txt")` и записывает в него `"hello from fixture"`, выводя `"SETUP"`; затем (2) передаёт путь через yield; затем (3) удаляет файл и выводит `"TEARDOWN"`. Напишите тест, который читает файл и проверяет его содержимое. Запустите `pytest -v -s`, чтобы увидеть SETUP и TEARDOWN вокруг теста.
# conftest.py
import pytest
from pathlib import Path
@pytest.fixture
def temp_file():
path = Path('test_output.txt')
# записать 'hello from fixture' в path, вывести 'SETUP'
# yield path
# удалить файл, вывести 'TEARDOWN'
pass
# test_tempfile.py
def test_reads_fixture_file(temp_file):
# прочитать temp_file и проверить содержимое == 'hello from fixture'
pass
Мы используем необходимые 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 с
Для активации режима слепого набора не подсматривайте на физическую клавиатуру.