Python · Тестування з pytest · Експертний

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

5 завдань

Організовуйте великі тест-сьюти для швидкості, ізоляції та зручності підтримки.

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

#
Один каталог `tests/` з 20 файлами є керованим. При 200 файлах у 5 модулях стає незрозуміло, куди додавати нові тести, які тести безпечно запускати швидко і чому раніше успішний тест раптово провалюється на свіжій гілці. Архітектура тестів -- це набір рішень, що зберігають ваш тест-сьют зрозумілим, надійним і швидким у міру його зростання. ## Трирівнева модель Тести природно поділяються на три категорії залежно від того, чого вони торкаються: **Юніт-тести** тестують одну функцію або клас в ізоляції. Вони не мають введення-виведення -- жодної бази даних, мережі, файлової системи. Вони виконуються за мілісекунди. Тест-сьют з 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 функції), або `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-сьют**: запускається вночі або на release-гілках. Для запуску підмножин комбінуйте адресацію шляхів pytest та фільтрацію міток: ```bash # Лише юніт-тести (швидко) pytest tests/unit/ # Лише інтеграційні тести pytest tests/integration/ # Все, крім повільних тестів pytest -m "not slow" # Лише тести для конкретного модуля pytest tests/unit/test_services.py ``` ## Реєстрація кастомних міток pytest попереджає, коли тест використовує незареєстровану мітку. Реєструйте мітки в `pytest.ini`: ```ini [pytest] markers = unit: fast tests with no I/O integration: tests that use the database or network slow: tests that take more than 1 second smoke: a minimal subset run before deployment ``` `--strict-markers` (також в `addopts`) змушує pytest негайно завершуватися з помилкою, якщо тест використовує незареєстровану мітку -- ловить опечатки на кшталт `@pytest.mark.integraion` до того, як вони мовчки стають відсутніми операціями. ## Поширені антипатерни **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) ``` Ці фікстури створюють об'єкти в пам'яті, не торкаючись бази даних. Вони виконуються в scope функції (за замовчуванням), тому кожен тест отримує свіжий об'єкт. ## 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` -- запуск live-сервера є дорогим, тому він поділяється на весь тестовий сеанс. ## Мітки та pytest.ini ```ini [pytest] addopts = -v --tb=short --strict-markers testpaths = tests markers = unit: fast tests with no I/O integration: tests that use the database or network slow: tests that take more than 1 second smoke: minimal subset run before deployment ``` Застосовуйте мітки на рівні модуля, щоб уникати їх повторення для кожної функції: ```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 ``` ## Правила scope 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: fast tests, no I/O integration: uses database or network slow: takes more than 1 second smoke: minimal pre-deploy check ``` ## Запуск підмножин ```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_` | Витягти спільне налаштування у фікстуру | | Спільний мутабельний стан | Тести провалюються у випадковому порядку | Використовувати фікстури 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: fast tests with no I/O
    integration: tests that use the database or network
    slow: tests that take more than 1 second


# 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 з фікстурами різних scope

#

Створіть три файли `conftest.py` для тест-сьюту з такою структурою: ``` tests/ ├── conftest.py ├── unit/ │ └── conftest.py └── integration/ └── conftest.py ``` Вимоги: - `tests/conftest.py`: визначте фікстуру з **scope сесії** `app_config`, що повертає dict `{"env": "test", "debug": False}`. Ця фікстура має бути доступна всім тестам. - `tests/unit/conftest.py`: визначте фікстуру з **scope функції** `calculator`, що повертає новий екземпляр `Calculator()`. Доступна лише для юніт-тестів. - `tests/integration/conftest.py`: визначте фікстуру з **scope функції** `db_session`, що виводить `"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 з scope сесії тут


# tests/unit/conftest.py
import pytest

# фікстура calculator з scope функції тут


# tests/integration/conftest.py
import pytest

# фікстура db_session з scope функції тут


# 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: fast tests integration: database tests slow: tests over 1 second ``` 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