Python · Тестирование с pytest · Экспертный

Архитектура большого тест-сьюта

5 задач

Организуйте большие тест-сьюты для скорости, изоляции и удобства поддержки.

Структурирование большого тест-сьюта

#
Один каталог `tests/` с 20 файлами -- это терпимо. При 200 файлах в 5 модулях становится неясно, куда добавлять новые тесты, какие тесты можно запускать быстро, и почему ранее проходивший тест внезапно падает на свежей ветке. Архитектура тестов -- это набор решений, которые сохраняют ваш тест-сьют навигируемым, надёжным и быстрым по мере роста. ## Трёхслойная модель Тесты естественным образом делятся на три категории в зависимости от того, что они затрагивают: **Юнит-тесты** проверяют одну функцию или класс в изоляции. В них нет I/O -- ни базы данных, ни сети, ни файловой системы. Они выполняются за миллисекунды. Тест-сьют из 500 юнит-тестов типично завершается менее чем за две секунды. **Интеграционные тесты** проверяют как компоненты работают вместе: вью + база данных, сервис + кэш, парсер + ридер файлов. Они настраивают реальную инфраструктуру (тестовую базу данных, запущенный сервер, временный каталог) и медленнее -- часто 100-500 мс на тест. **Сквозные тесты (e2e)** управляют приложением снаружи, обычно через браузер (Selenium, Playwright) или реальный HTTP-клиент против запущенного сервера. Они самые медленные и хрупкие, но ловят регрессии, которые юнит- и интеграционные тесты упустить не могут. Стандартная структура каталогов отражает это разделение: ``` tests/ +-- conftest.py # общие фикстуры для всего тест-сьюта +-- unit/ | +-- conftest.py # фикстуры только для юнит-тестов | +-- test_models.py | +-- test_services.py | +-- test_utils.py +-- integration/ | +-- conftest.py # фикстуры только для интеграционных тестов | +-- test_api.py | +-- test_db.py +-- e2e/ +-- conftest.py +-- test_checkout_flow.py ``` ## Иерархия conftest.py pytest ищет файлы `conftest.py` начиная с каталога тестового файла и поднимаясь до `rootdir`. Фикстура, определённая в `tests/conftest.py`, доступна каждому тесту в тест-сьюте. Фикстура, определённая в `tests/unit/conftest.py`, доступна только тестам под `tests/unit/`. Это слоение позволяет избежать распространённого антипаттерна: одного гигантского `conftest.py` в корне, определяющего всё. Когда юнит-тесты импортируют интеграционные фикстуры (сессии базы данных, HTTP-клиенты), они непреднамеренно наследуют издержки настройки -- и граница между "быстрыми" и "медленными" тестами стирается. Правило большого пальца: **фикстура должна быть определена в самом высоком conftest, где она нужна, и не выше.** Фикстура базы данных, используемая только интеграционными тестами, принадлежит `tests/integration/conftest.py`, а не корневому. ## Принципы изоляции тестов **Никакого общего изменяемого состояния.** Если тест A модифицирует глобальный объект (список на уровне модуля, переменную класса, синглтон), а тест B читает этот объект, результат теста B зависит от того, был ли сначала запущен тест A. Это скрытая зависимость от порядка. Решения: используйте свежие экземпляры для каждого теста (фикстуры со scope function), или `monkeypatch` для восстановления состояния после каждого теста. **Никакой зависимости от порядка.** Ваш тест-сьют должен давать одинаковые результаты независимо от того, запускает ли pytest тесты по алфавиту, в обратном порядке или случайным образом (`pytest-randomly`). Если изменение порядка меняет результаты, тест не изолирован по-настоящему. Запустите `pytest --randomly-seed=last` для воспроизведения конкретного порядка, или `pytest -p randomly --randomly-seed=0` для включения рандомизации. **Никакой коммуникации между тестами.** Тесты не должны передавать данные друг другу через общие переменные. Если вы пишете `test_02_login_after_register`, логика настройки принадлежит фикстуре, а не распределению по тестовым функциям с нумерацией. ## Стратегия производительности Цель -- цикл обратной связи, достаточно быстрый чтобы разработчики запускали полный юнит-сьют перед каждым коммитом, и интеграционные тесты в CI при каждом push. Типичные цели: - **Юнит-сьют**: менее 5 секунд локально, запускается всегда. - **Интеграционный сьют**: менее 2 минут в CI, запускается при push. - **E2e-сьют**: запускается ночью или на релизных ветках. Для запуска подмножеств комбинируйте нацеливание по пути и фильтрацию по меткам: ```bash # Только юнит-тесты (быстро) pytest tests/unit/ # Только интеграционные тесты pytest tests/integration/ # Всё кроме медленных тестов pytest -m "not slow" # Только тесты конкретного модуля pytest tests/unit/test_services.py ``` ## Регистрация кастомных меток pytest предупреждает когда тест использует незарегистрированную метку. Регистрируйте метки в `pytest.ini`: ```ini [pytest] markers = unit: быстрые тесты без I/O integration: тесты использующие базу данных или сеть slow: тесты выполняющиеся более 1 секунды smoke: минимальный набор перед деплоем ``` `--strict-markers` (также в `addopts`) заставляет pytest немедленно завершаться с ошибкой если тест использует незарегистрированную метку -- это ловит опечатки вроде `@pytest.mark.integraion`, которые иначе молча становятся no-op. ## Распространённые антипаттерны **God conftest.** Один `conftest.py` с 50 фикстурами, неявно импортируемый каждым тестом. Тяжело навигировать, скрывает связанность между слоями тестов. **Тесты, которые убирают за собой (вместо использования фикстур).** Если настройка выполняется в теле теста, а завершение -- в `try/finally`, система фикстур обходится стороной. Завершение фикстур в pytest выполняется предсказуемо и корректно; ручной `try/finally` внутри тестов ненадёжен и скрывает намерение. **Злоупотребление фикстурами.** Не всё нуждается в фикстуре. Простое `user = User(name="Alice")` в теле теста понятнее, чем фикстура `alice_user`, делающая то же самое и используемая в 15 тестах. Используйте фикстуры для настройки, которая действительно переиспользуется или включает завершение. **Неявно упорядоченные тесты.** Именование тестов `test_01_`, `test_02_` для управления порядком -- это запах. Если порядок важен, тесты разделяют состояние, которого не должны. Извлеките общее состояние в фикстуру или вспомогательную функцию, вызываемую каждым тестом независимо.

Архитектура тестов в среднем Django-проекте

#
Это пошаговое руководство показывает как выглядит хорошо структурированный тест-сьют для Django e-commerce проекта: каталог товаров, аккаунты пользователей, сервис заказов и публичное API. Цель -- тест-сьют, где быстрая обратная связь занимает 3 секунды, а полный сьют в CI выполняется менее чем за 90 секунд. ## Дерево каталогов ``` my_shop/ +-- src/ | +-- catalogue/ | +-- orders/ | +-- accounts/ +-- tests/ | +-- conftest.py | +-- unit/ | | +-- conftest.py | | +-- catalogue/ | | | +-- test_models.py | | | +-- test_services.py | | +-- orders/ | | | +-- test_pricing.py | | | +-- test_validators.py | | +-- accounts/ | | +-- test_password_policy.py | +-- integration/ | | +-- conftest.py | | +-- test_order_flow.py | | +-- test_api_products.py | | +-- test_api_checkout.py | +-- e2e/ | +-- conftest.py | +-- test_checkout_browser.py +-- pytest.ini +-- Makefile ``` Дерево исходников зеркалируется в дереве тестов под `tests/unit/`. Это упрощает поиск тестов для модуля: `catalogue/models.py` → `tests/unit/catalogue/test_models.py`. ## Корневой conftest.py `tests/conftest.py` содержит только то, что нужно каждому тесту: ```python # tests/conftest.py import pytest # django_db_setup предоставляется pytest-django автоматически. # Переопределяйте только если нужна кастомная логика создания БД (редко). @pytest.fixture def settings_override(settings): # Безопасное переопределение настроек -- изменения откатываются после каждого теста. settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' return settings ``` Держите этот файл маленьким. Правило: если вы добавляете сюда фикстуру, и 90% тестов никогда её не используют, переместите её в conftest соответствующего подкаталога. ## Unit conftest.py ```python # tests/unit/conftest.py import pytest from catalogue.models import Product from accounts.models import User @pytest.fixture def product(): # Product в памяти -- база данных не нужна. return Product(name="Widget", price=9_99, stock=10) @pytest.fixture def user(): return User(email="[email protected]", is_active=True) ``` Эти фикстуры создают объекты в памяти без обращения к базе данных. Они работают в области функции (по умолчанию), поэтому каждый тест получает свежий объект. ## Integration conftest.py ```python # tests/integration/conftest.py import pytest @pytest.fixture(scope='session') def api_base_url(live_server): # Полный URL тестового сервера Django. return live_server.url @pytest.fixture def auth_client(client, django_user_model): # Авторизованный тестовый клиент Django. user = django_user_model.objects.create_user( username='testuser', password='testpass' ) client.force_login(user) return client @pytest.fixture def seeded_catalogue(db): # Три товара в базе данных. from catalogue.models import Product Product.objects.bulk_create([ Product(name='Widget', price=999, stock=10), Product(name='Gadget', price=1999, stock=5), Product(name='Doohickey', price=499, stock=0), ]) ``` Фикстура `db` (от pytest-django) помечает эти тесты как тесты с базой данных. Обратите внимание на `scope='session'` у `api_base_url` -- запуск живого сервера дорогой, поэтому он разделяется на всю тестовую сессию. ## Метки и pytest.ini ```ini [pytest] addopts = -v --tb=short --strict-markers testpaths = tests markers = unit: быстрые тесты без I/O integration: тесты использующие базу данных или сеть slow: тесты выполняющиеся более 1 секунды smoke: минимальный набор перед деплоем ``` Применяйте метки на уровне модуля чтобы не повторять их на каждой функции: ```python # tests/integration/test_order_flow.py import pytest pytestmark = [pytest.mark.integration] @pytest.mark.django_db def test_order_created_on_checkout(auth_client, seeded_catalogue): resp = auth_client.post('/api/orders/', {'product_id': 1, 'quantity': 2}) assert resp.status_code == 201 @pytest.mark.django_db @pytest.mark.slow def test_order_email_sent(auth_client, seeded_catalogue, mailoutbox): auth_client.post('/api/orders/', {'product_id': 1, 'quantity': 1}) assert len(mailoutbox) == 1 assert 'Order confirmation' in mailoutbox[0].subject ``` `pytestmark` -- список на уровне модуля. Каждый тест в файле наследует все метки из списка. Добавление `pytest.mark.slow` к отдельным тяжёлым тестам позволяет исключать их из быстрого интеграционного прогона. ## Цели Makefile ```makefile .PHONY: test test-unit test-integration test-fast test-ci test-unit: pytest tests/unit/ -q test-integration: pytest tests/integration/ -q test-fast: pytest tests/unit/ tests/integration/ -m "not slow" -q test: pytest -q test-ci: pytest --cov=src --cov-report=term-missing --cov-fail-under=80 -q ``` `-q` (тихий режим) подавляет отдельные имена тестов и печатает только итог -- полезно для быстрых циклов обратной связи. ## Обнаружение и исправление зависимости от порядка тестов Вот реальный паттерн, создающий зависимости от порядка: ```python # СЛОМАНО -- тесты разделяют состояние через список на уровне модуля # (Product и client импортируются откуда-то; фокус на _created_ids) _created_ids = [] def test_create_product(): product = Product.objects.create(name='Widget', price=999) _created_ids.append(product.id) def test_created_product_appears_in_list(): # Проходит только если test_create_product запущен первым resp = client.get('/api/products/') assert any(p['id'] in _created_ids for p in resp.json()) ``` Исправление -- перенести настройку в фикстуру, делая каждый тест самодостаточным: ```python # ИСПРАВЛЕНО -- каждый тест независим @pytest.fixture def widget(db): return Product.objects.create(name='Widget', price=999) @pytest.mark.django_db def test_create_product(db): product = Product.objects.create(name='Widget', price=999) assert product.id is not None @pytest.mark.django_db def test_created_product_appears_in_list(widget, client): resp = client.get('/api/products/') ids = [p['id'] for p in resp.json()] assert widget.id in ids ``` Теперь каждый тест настраивает то, что ему нужно, и не зависит от другого теста, который был запущен первым. База данных откатывается между тестами транзакционными фикстурами pytest-django. Для проактивного обнаружения зависимостей от порядка установите `pytest-randomly` и запустите: ```bash pip install pytest-randomly pytest --randomly-seed=random ``` Если ваш тест-сьют падает с одним seed, но проходит с другим -- у вас скрытая зависимость от порядка.

Справочная карточка архитектуры тестов

#
## Стандартная структура каталогов ``` tests/ +-- conftest.py <- фикстуры для всех тестов +-- unit/ | +-- conftest.py <- фикстуры только для юнит-тестов | +-- module_name/ | +-- test_*.py +-- integration/ | +-- conftest.py <- фикстуры только для интеграционных тестов | +-- test_*.py +-- e2e/ +-- conftest.py +-- test_*.py ``` ## Правила области видимости conftest.py | Где определено | Доступно | |---|---| | `tests/conftest.py` | Всем тестам | | `tests/unit/conftest.py` | Только тестам под `tests/unit/` | | `tests/integration/conftest.py` | Только тестам под `tests/integration/` | Фикстуры, определённые ниже в дереве, **переопределяют** одноимённые фикстуры выше. ## Настройка меток в pytest.ini ```ini [pytest] addopts = -v --tb=short --strict-markers testpaths = tests markers = unit: быстрые тесты без I/O integration: использует базу данных или сеть slow: выполняется более 1 секунды smoke: минимальная проверка перед деплоем ``` ## Запуск подмножеств ```bash pytest tests/unit/ # только юнит-сьют pytest tests/integration/ # только интеграционный сьют pytest -m "not slow" # исключить медленные тесты pytest -m "smoke" # только smoke-тесты pytest -m "integration and not slow" # комбинирование меток pytest tests/unit/test_models.py # один файл pytest tests/unit/test_models.py::test_price_is_positive # один тест ``` ## Метка на уровне модуля ```python # Применяется к каждому тесту в файле pytestmark = [pytest.mark.integration, pytest.mark.django_db] ``` ## Обнаружение зависимостей от порядка ```bash pip install pytest-randomly pytest --randomly-seed=random # случайный порядок при каждом запуске pytest --randomly-seed=last # воспроизвести последний порядок pytest -p no:randomly # отключить рандомизацию ``` ## Цели Makefile ```makefile test-unit: pytest tests/unit/ -q test-integration: pytest tests/integration/ -q test-fast: pytest tests/unit/ tests/integration/ -m "not slow" -q test-ci: pytest --cov=src --cov-fail-under=80 -q ``` ## Распространённые антипаттерны | Антипаттерн | Симптом | Решение | |---|---|---| | God conftest | Корневой conftest с 50+ фикстурами | Перенести фикстуры в conftest подкаталога | | Нумерованные тесты | `test_01_`, `test_02_` | Извлечь общую настройку в фикстуру | | Общее изменяемое состояние | Тесты падают в случайном порядке | Использовать фикстуры с function scope вместо глобалей модуля | | Злоупотребление фикстурами | Фикстура `alice_user` используется в 1 тесте | Создавать объект прямо в теле теста | | Смешанные слои | Юнит-тесты импортируют фикстуру `db` | Держать unit conftest без I/O фикстур | | Ручное завершение в теле теста | `try/finally` внутри тестовых функций | Использовать `yield`-фикстуры -- они выполняются даже при ошибке |
01

Реорганизовать плоский каталог тестов в unit/ и integration/

#

У вас есть плоский каталог `tests/` с такими файлами: ``` tests/ +-- test_models.py <- тесты моделей Product и Order в памяти (без БД) +-- test_services.py <- тесты чистых вычислительных функций (без БД) +-- test_api.py <- тесты API-эндпоинтов с реальной базой данных +-- test_db_queries.py <- тесты методов запросов к БД (нужна БД) ``` Реорганизуйте это в следующую структуру. Напишите новые пути файлов и `pytest.ini`, устанавливающий `testpaths = tests` чтобы pytest автоматически находил тесты. Целевая структура: ``` tests/ +-- conftest.py <- пустой пока что (просто создать файл) +-- unit/ | +-- conftest.py <- пустой пока что | +-- test_models.py | +-- test_services.py +-- integration/ +-- conftest.py <- пустой пока что +-- test_api.py +-- test_db_queries.py ``` Также: какая команда запускает только интеграционные тесты после реорганизации?

# Перечислите новые пути файлов (по одному на строку):
# tests/conftest.py
# ...


# Содержимое pytest.ini:
# [pytest]
# testpaths = ...


# Команда для запуска только интеграционных тестов:
# pytest ...
Решение
# Новые пути файлов после реорганизации:
# tests/conftest.py
# tests/unit/conftest.py
# tests/unit/test_models.py
# tests/unit/test_services.py
# tests/integration/conftest.py
# tests/integration/test_api.py
# tests/integration/test_db_queries.py

# pytest.ini (создайте этот файл в корне проекта):
# [pytest]
# testpaths = tests

# Команда для запуска только интеграционных тестов:
# pytest tests/integration/
02

Зарегистрировать метки и применить их к тестам

#

У вас есть этот `pytest.ini` и два тестовых файла. Ваша задача: 1. Добавить `addopts = --strict-markers` в `pytest.ini` 2. Зарегистрировать три метки: `unit`, `integration`, `slow` 3. Применить метки к тестовым файлам через `pytestmark` (на уровне модуля) ```ini # pytest.ini -- текущее состояние [pytest] testpaths = tests ``` ```python # tests/unit/test_pricing.py def test_discount_applied(): assert apply_discount(100, 0.1) == 90 def test_negative_discount_raises(): with pytest.raises(ValueError): apply_discount(100, -0.1) ``` ```python # tests/integration/test_orders.py def test_order_saved_to_db(db): order = Order.objects.create(total=100) assert Order.objects.count() == 1 def test_large_order_sends_email(db, mailoutbox): # занимает 2 секунды Order.objects.create(total=10000) assert len(mailoutbox) == 1 ``` Напишите обновлённый `pytest.ini` и оба тестовых файла с добавленным `pytestmark`. Пометьте `test_large_order_sends_email` как `integration` и `slow` одновременно.

# pytest.ini
[pytest]
testpaths = tests
# добавить addopts и markers здесь


# tests/unit/test_pricing.py
import pytest
# добавить pytestmark здесь

def test_discount_applied():
    assert apply_discount(100, 0.1) == 90

def test_negative_discount_raises():
    with pytest.raises(ValueError):
        apply_discount(100, -0.1)


# tests/integration/test_orders.py
import pytest
# добавить pytestmark здесь

def test_order_saved_to_db(db):
    order = Order.objects.create(total=100)
    assert Order.objects.count() == 1

def test_large_order_sends_email(db, mailoutbox):
    Order.objects.create(total=10000)
    assert len(mailoutbox) == 1
Решение
# pytest.ini
[pytest]
testpaths = tests
addopts = --strict-markers
markers =
    unit: быстрые тесты без I/O
    integration: тесты использующие базу данных или сеть
    slow: тесты выполняющиеся более 1 секунды


# tests/unit/test_pricing.py
import pytest

pytestmark = [pytest.mark.unit]

def test_discount_applied():
    assert apply_discount(100, 0.1) == 90

def test_negative_discount_raises():
    with pytest.raises(ValueError):
        apply_discount(100, -0.1)


# tests/integration/test_orders.py
import pytest

pytestmark = [pytest.mark.integration]

def test_order_saved_to_db(db):
    order = Order.objects.create(total=100)
    assert Order.objects.count() == 1

@pytest.mark.slow
def test_large_order_sends_email(db, mailoutbox):
    Order.objects.create(total=10000)
    assert len(mailoutbox) == 1
03

Построить иерархию conftest.py с фикстурами разной области

#

Создайте три файла `conftest.py` для тест-сьюта с такой структурой: ``` tests/ +-- conftest.py +-- unit/ | +-- conftest.py +-- integration/ +-- conftest.py ``` Требования: - `tests/conftest.py`: определить фикстуру `app_config` с **областью session**, возвращающую словарь `{"env": "test", "debug": False}`. Эта фикстура должна быть доступна всем тестам. - `tests/unit/conftest.py`: определить фикстуру `calculator` с **областью function**, возвращающую новый экземпляр `Calculator()`. Доступна только юнит-тестам. - `tests/integration/conftest.py`: определить фикстуру `db_session` с **областью function**, которая печатает `"opening db"` перед yield строки `"db_connection"` и печатает `"closing db"` после. Доступна только интеграционным тестам. Также напишите тестовую функцию в `tests/unit/test_calc.py`, использующую обе `app_config` и `calculator`, и тестовую функцию в `tests/integration/test_db.py`, использующую обе `app_config` и `db_session`.

# tests/conftest.py
import pytest

# фикстура app_config с областью session здесь


# tests/unit/conftest.py
import pytest

# фикстура calculator с областью function здесь


# tests/integration/conftest.py
import pytest

# фикстура db_session с областью function здесь


# tests/unit/test_calc.py
# использовать app_config и calculator


# tests/integration/test_db.py
# использовать app_config и db_session
Решение
# tests/conftest.py
import pytest

@pytest.fixture(scope='session')
def app_config():
    return {"env": "test", "debug": False}


# tests/unit/conftest.py
import pytest

class Calculator:
    def add(self, a, b):
        return a + b

@pytest.fixture
def calculator():
    return Calculator()


# tests/integration/conftest.py
import pytest

@pytest.fixture
def db_session():
    print("opening db")
    yield "db_connection"
    print("closing db")


# tests/unit/test_calc.py
def test_add(app_config, calculator):
    assert app_config["env"] == "test"
    assert calculator.add(2, 3) == 5


# tests/integration/test_db.py
def test_connect(app_config, db_session):
    assert app_config["env"] == "test"
    assert db_session == "db_connection" 
04

Написать Makefile с целями для тестирования

#

Напишите `Makefile` для проекта с такой структурой тестов: ``` tests/ +-- unit/ +-- integration/ ``` И таким `pytest.ini`: ```ini [pytest] testpaths = tests addopts = --strict-markers markers = unit: быстрые тесты integration: тесты базы данных slow: тесты дольше 1 секунды ``` Makefile должен иметь четыре цели: | Цель | Что запускает | |---|---| | `make test-unit` | Только юнит-тесты, тихий вывод | | `make test-integration` | Только интеграционные тесты, тихий вывод | | `make test-fast` | Юнит + интеграционные, исключая медленные, тихий вывод | | `make test-ci` | Полный сьют с покрытием (src/), провал ниже 80%, тихий вывод | Все цели должны быть объявлены как `.PHONY`.

# Makefile
.PHONY: ...

test-unit:
	...

test-integration:
	...

test-fast:
	...

test-ci:
	...
Решение
# Makefile
.PHONY: test-unit test-integration test-fast test-ci

test-unit:
	pytest tests/unit/ -q

test-integration:
	pytest tests/integration/ -q

test-fast:
	pytest tests/unit/ tests/integration/ -m "not slow" -q

test-ci:
	pytest --cov=src --cov-report=term-missing --cov-fail-under=80 -q
05

Найти и исправить зависимость от порядка тестов

#

Следующий тестовый файл проходит когда тесты запускаются в порядке по умолчанию, но падает при другом порядке. Представьте запуск в обратном порядке: ``` test_empty_cart_total → test_remove_item_from_cart → test_cart_total → test_add_item_to_cart ``` Найдите зависимость от порядка и исправьте её так чтобы тесты проходили в любом порядке. **Подсказка:** Установите `pytest-randomly` и запустите `pytest --randomly-seed=0` чтобы убедиться что ваше исправление работает независимо от порядка. ```python # test_cart.py import pytest _cart = [] # общее состояние на уровне модуля def test_add_item_to_cart(): _cart.append({"id": 1, "name": "Widget", "qty": 2}) assert len(_cart) == 1 def test_cart_total(): total = sum(item["qty"] * 10 for item in _cart) assert total == 20 # 2 x 10 def test_remove_item_from_cart(): _cart.clear() assert len(_cart) == 0 def test_empty_cart_total(): total = sum(item["qty"] * 10 for item in _cart) assert total == 0 ``` **Часть 1 -- Определить:** Какой тест падает первым при обратном порядке? Почему? **Часть 2 -- Исправить:** Перепишите файл так чтобы все четыре теста были независимы. Используйте pytest-фикстуру вместо `_cart` на уровне модуля.

# Часть 1 -- Какой тест падает первым в обратном порядке и почему?
# (напишите ответ в комментарии)
#
# Первым падает тест: ...
# Причина: ...


# Часть 2 -- Исправленный test_cart.py
import pytest

# определить фикстуру cart здесь


def test_add_item_to_cart(cart):
    pass  # реализовать


def test_cart_total(cart):
    pass  # реализовать


def test_remove_item_from_cart(cart):
    pass  # реализовать


def test_empty_cart_total(cart):
    pass  # реализовать
Решение
# Часть 1 -- Какой тест падает первым в обратном порядке?
#
# Первым падает тест: test_cart_total
# Причина: В обратном порядке выполнение:
#   test_empty_cart_total -> test_remove_item_from_cart -> test_cart_total -> test_add_item_to_cart
# test_empty_cart_total запускается первым; _cart уже пуст, total == 0 -- случайно ПРОХОДИТ.
# test_remove_item_from_cart очищает уже пустой список -- ПРОХОДИТ.
# test_cart_total запускается следующим; _cart всё ещё пуст, total == 0, а не 20 -- ПАДАЕТ.
#
# Корневая причина: _cart -- список на уровне модуля, общий для всех тестов.
# Тесты неявно коммуницируют через него, поэтому результаты зависят от порядка запуска.


# Часть 2 -- Исправленный test_cart.py
import pytest


@pytest.fixture
def cart():
    return []


def test_add_item_to_cart(cart):
    cart.append({"id": 1, "name": "Widget", "qty": 2})
    assert len(cart) == 1


def test_cart_total(cart):
    cart.append({"id": 1, "name": "Widget", "qty": 2})
    total = sum(item["qty"] * 10 for item in cart)
    assert total == 20


def test_remove_item_from_cart(cart):
    cart.append({"id": 1, "name": "Widget", "qty": 2})
    cart.clear()
    assert len(cart) == 0


def test_empty_cart_total(cart):
    total = sum(item["qty"] * 10 for item in cart)
    assert total == 0