Python · Тестування з pytest · Просунутий

Інтеграційне тестування

5 завдань

Тестуйте кілька компонентів разом у взаємодії з реальними сервісами.

Юніт vs інтеграційний vs наскрізний: коли що використовувати

#
## Піраміда тестів Не всі тести слугують одній меті. Здоровий тест-сьют використовує три окремі шари: ``` ┌─────────────────┐ │ End-to-End │ мало, повільні, тестують всю систему ├─────────────────┤ │ Інтеграційні │ більше, тестують реальні компоненти разом ├─────────────────┤ │ Юніт │ багато, швидкі, тестують одне в ізоляції └─────────────────┘ ``` Більшість проектів має мати багато юніт-тестів, помірну кількість інтеграційних і невелику кількість наскрізних тестів. Така форма є класичною, оскільки кожен шар має різні витрати і переваги. ## Юніт-тести: швидкі та ізольовані Юніт-тест виконує єдину функцію або клас у повній ізоляції. Кожна зовнішня залежність -- бази даних, HTTP API, файлова система -- замінюється моком або заглушкою. ```python # Юніт-тест: без реального HTTP-запиту def test_get_product_count_success(): with patch('myapp.requests.get') as mock_get: mock_get.return_value.json.return_value = {'count': 42} mock_get.return_value.status_code = 200 result = get_product_count() assert result == 42 ``` **Переваги:** мілісекунди на тест, без зовнішніх залежностей, точне визначення місця збою. **Обмеження:** моки можуть відходити від реальності. Якщо API повертає `{'total': 42}` замість `{'count': 42}`, юніт-тест все одно проходить -- мок був неправильним. ## Інтеграційні тести: реальні компоненти, реальні контракти Інтеграційний тест виконує кілька реальних компонентів разом. Для тестування API це означає виконання реальних HTTP-запитів до реального сервера, розбір реального JSON, перевірку реальних кодів статусу. ```python # Інтеграційний тест: реальний HTTP-запит до справжнього сервера def test_get_product_count(): resp = requests.get('https://apilearn.tukas.dev/api/products/') assert resp.status_code == 200 data = resp.json() assert 'results' in data result = len(data['results']) assert result >= 0 ``` **Переваги:** виявляє реальні інтеграційні баги -- невідповідності схеми, збої автентифікації, несподівані редиректи, справжні форми відповідей. Тест використовує той самий HTTP-шлях, що і ваш продакшн-код. **Витрати:** повільніші (мережевий round-trip), можуть бути нестабільними (проблеми з мережею або сервером), потребують реального сервера, можуть залишати дані. ## Наскрізні тести: вся система Наскрізний тест керує всією системою через її реальний інтерфейс -- зазвичай браузер для веб-застосунків або повну поверхню CLI/API. Він тестує, що всі шари (фронтенд, бекенд, база даних) працюють разом. Наскрізні тести найдорожчі: повільні, крихкі, складні в налагодженні. Використовуйте їх рідко -- для найбільш критичних користувацьких сценаріїв. ## Що робить тест інтеграційним? Тест є інтеграційним, якщо він: - Виконує реальні мережеві виклики (HTTP, gRPC, WebSocket) - Читає або записує у реальну базу даних - Читає або записує реальні файли на диску - Використовує реальні зовнішні сервіси (провайдери автентифікації, платіжні шлюзи, черги) Якщо тест замість цього використовує моки, він є юніт-тестом незалежно від того, скільки функцій він викликає всередині. ## Витрати інтеграційних тестів **Повільні:** мережевий round-trip займає 50-500 мс. Юніт-тест займає 0.1 мс. Сто інтеграційних тестів займуть 5-50 секунд; сто юніт-тестів -- 0.01 секунди. **Нестабільні:** мережеві збої, таймаути, ліміти запитів і перезапуски сервера -- все це спричиняє провали інтеграційних тестів з причин, не пов'язаних з вашим кодом. Нестабільний тест-сьют підриває довіру до CI. **Забруднення стану:** інтеграційні тести можуть створювати дані, що впливають на наступні тести. Тест, який створює користувача, але не видаляє його, може спричинити збій іншого тесту, що перевіряє "немає користувачів з цим username". **Залежності:** інтеграційні тести потребують запущеного реального сервера. У CI це означає розгортання тестового середовища, очікування його готовності, а потім запуск тестів. ## Стратегії ізоляції тестів **Використовуйте унікальні ідентифікатори:** при створенні користувачів або ресурсів генеруйте унікальні значення (мітки часу, UUID), щоб тести не конфліктували між собою або з наявними даними: ```python import time def test_register_and_login(): username = f'testuser_{int(time.time())}' ... ``` **Прибирайте за собою:** якщо тест створює ресурс, видаліть його в кінці. Використовуйте для цього teardown фікстури: ```python @pytest.fixture def temp_user(): username = f'tmp_{int(time.time())}' # setup: створити користувача resp = requests.post(BASE_URL + '/api/users/register/', json={...}) yield {'username': username, 'token': get_token(username)} # teardown: видалити користувача requests.delete(BASE_URL + f'/api/users/{username}/', headers=auth_headers) ``` **Використовуйте виділений тестовий акаунт:** для тестів, що читають дані (а не пишуть), автентифікуйтеся з відомим тестовим акаунтом, а не створюйте нових користувачів. Акаунт `testuser` / `TestUser2024!` на `apilearn.tukas.dev` підходить для цього. **Session-scoped автентифікація:** якщо багато тестів потребують автентифікації, увійдіть один раз за допомогою session-scoped фікстури і спільно використовуйте токен. Це скорочує накладні витрати на вхід з N round-trips до 1. ## Запуск інтеграційних тестів окремо від юніт-тестів Позначте інтеграційні тести і запускайте їх окремо: ```ini # pytest.ini markers = integration: marks tests that make real network calls ``` ```python @pytest.mark.integration def test_products_api(): ... ``` ```bash pytest -m integration # лише інтеграційні тести pytest -m "not integration" # лише юніт-тести (швидкий CI) ``` Це дозволяє запускати швидкий юніт-набір при кожному коміті, а повільніший інтеграційний -- щоночі або за потребою. ## Порівняння двох підходів Та сама функціональність, реалізована як юніт-тест і як інтеграційний тест: ```python # Функція, що тестується import requests def get_product_count(base_url='https://apilearn.tukas.dev'): resp = requests.get(f'{base_url}/api/products/') resp.raise_for_status() return resp.json()['count'] # Юніт-тест: швидкий, ізольований, але мок може відходити від реальності def test_get_product_count_unit(): from unittest.mock import patch with patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {'count': 5} mock_get.return_value.raise_for_status.return_value = None assert get_product_count() == 5 # Інтеграційний тест: повільніший, але тестує реальний контракт API def test_get_product_count_integration(): import requests resp = requests.get('https://apilearn.tukas.dev/api/products/') assert resp.status_code == 200 data = resp.json() assert 'count' in data or 'results' in data # що б API не повертав ``` Зверніть увагу, що юніт-тест припускає, що API повертає `{'count': 5}`. Якщо реальний API повертає `{'results': [...], 'count': 5}`, юніт-тест перевіряє мок, що відповідає реальності -- добре. Але якщо поле насправді називається `'total'`, юніт-тест проходить, тоді як інтеграційний виявляє невідповідність.

Інтеграційні тести на практиці: потоки автентифікації та спільні сесії

#
## Налаштування проекту ```bash pip install requests pytest ``` ```ini # pytest.ini [pytest] markers = integration: marks tests that make real network calls ``` Всі приклади використовують `https://apilearn.tukas.dev`. ## Приклад 1: потік автентифікації -- вхід і доступ до захищеного ендпоінту ```python # test_auth_flow.py import requests BASE_URL = 'https://apilearn.tukas.dev' def test_login_and_profile(): # Крок 1: автентифікація auth_resp = requests.post(f'{BASE_URL}/api/auth/token/', json={ 'username': 'testuser', 'password': 'TestUser2024!', }) assert auth_resp.status_code == 200 token = auth_resp.json()['token'] assert token # непорожній рядок # Крок 2: використати токен для доступу до захищеного ендпоінту headers = {'Authorization': f'Token {token}'} profile_resp = requests.get(f'{BASE_URL}/api/users/profile/', headers=headers) assert profile_resp.status_code == 200 profile = profile_resp.json() assert profile['username'] == 'testuser' ``` Це справжній інтеграційний тест: він виконує два реальні HTTP-запити, розбирає реальний JSON і перевіряє реальну відповідь. Якщо сервіс автентифікації змінить назву поля токена, або ендпоінт профілю змінить `username` на `user`, цей тест одразу це виявить. ## Приклад 2: session-scoped фікстура автентифікації Вхід при кожному тесті -- це повільно і марнотратно. Session-scoped фікстура виконує вхід один раз: ```python # conftest.py import pytest import requests BASE_URL = 'https://apilearn.tukas.dev' @pytest.fixture(scope='session') def base_url(): return BASE_URL @pytest.fixture(scope='session') def auth_headers(base_url): resp = requests.post(f'{base_url}/api/auth/token/', json={ 'username': 'testuser', 'password': 'TestUser2024!', }) resp.raise_for_status() token = resp.json()['token'] return {'Authorization': f'Token {token}'} ``` ```python # test_protected.py def test_profile_username(base_url, auth_headers): resp = requests.get(f'{base_url}/api/users/profile/', headers=auth_headers) assert resp.status_code == 200 assert resp.json()['username'] == 'testuser' def test_profile_has_email(base_url, auth_headers): resp = requests.get(f'{base_url}/api/users/profile/', headers=auth_headers) assert resp.status_code == 200 assert '@' in resp.json().get('email', '') def test_products_authenticated(base_url, auth_headers): resp = requests.get(f'{base_url}/api/products/', headers=auth_headers) assert resp.status_code == 200 ``` Один HTTP-вхід на весь тестовий сеанс. Всі три тести спільно використовують той самий словник `auth_headers`. Session-scope тут доречний, оскільки токен не змінюється між тестами. ## Приклад 3: пагінація як інтеграційний аспект Контракти пагінації (розмір сторінки, кількість результатів, URL `next`) -- саме те, що юніт-тести з моками не можуть перевірити: ```python # test_pagination.py def test_default_page_returns_results(base_url): resp = requests.get(f'{base_url}/api/products/') assert resp.status_code == 200 data = resp.json() assert 'results' in data assert len(data['results']) > 0 def test_page_size_respected(base_url): resp = requests.get(f'{base_url}/api/products/?page_size=2') assert resp.status_code == 200 results = resp.json()['results'] assert len(results) <= 2 def test_page_two_exists_when_enough_products(base_url): first = requests.get(f'{base_url}/api/products/?page=1&page_size=2') assert first.status_code == 200 data = first.json() if data.get('next'): second = requests.get(f'{base_url}/api/products/?page=2&page_size=2') assert second.status_code == 200 assert isinstance(second.json()['results'], list) ``` ## Приклад 4: юніт-тест vs інтеграційний тест -- пліч-о-пліч Та сама функція `get_product_names`, протестована двома способами: ```python # app.py import requests def get_product_names(base_url='https://apilearn.tukas.dev'): resp = requests.get(f'{base_url}/api/products/') resp.raise_for_status() return [p['name'] for p in resp.json()['results']] ``` ```python # Юніт-тест -- швидкий, але мок може розходитися з реальним API from unittest.mock import patch def test_get_product_names_unit(): with patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.raise_for_status.return_value = None mock_get.return_value.json.return_value = { 'results': [{'name': 'Widget'}, {'name': 'Gadget'}] } names = get_product_names() assert names == ['Widget', 'Gadget'] # Цей тест проходить, навіть якщо реальний API повертає {'name': None} для деяких елементів. # Інтеграційний тест -- повільніший, але перевіряє реальний контракт def test_get_product_names_integration(): names = get_product_names() # Не знаємо точних значень, тому перевіряємо структуру: assert isinstance(names, list) assert len(names) > 0 assert all(isinstance(n, str) for n in names) assert all(len(n) > 0 for n in names) ``` Інтеграційний тест не може перевіряти точні значення (назви продуктів змінюються), але верифікує контракт: `get_product_names` повертає непорожній список непорожніх рядків. Він провалиться, якщо API поверне `null` для `name`, або якщо структура зміниться -- саме ті баги, що мають значення у продакшні. ## Приклад 5: потік реєстрації з унікальними даними Коли тести створюють дані, використовуйте унікальні ідентифікатори для уникнення колізій: ```python # test_registration.py import time import requests BASE_URL = 'https://apilearn.tukas.dev' def test_register_and_login(): # Генеруємо унікальний username для цього запуску тестів username = f'pytest_{int(time.time())}' # Крок 1: реєстрація reg_resp = requests.post(f'{BASE_URL}/api/users/register/', json={ 'username': username, 'email': f'{username}@example.com', 'password': 'Secure123!', 'first_name': 'Test', 'last_name': 'User', }) assert reg_resp.status_code in (200, 201) # Крок 2: вхід з новими обліковими даними auth_resp = requests.post(f'{BASE_URL}/api/auth/token/', json={ 'username': username, 'password': 'Secure123!', }) assert auth_resp.status_code == 200 token = auth_resp.json()['token'] # Крок 3: перевірка профілю profile_resp = requests.get(f'{BASE_URL}/api/users/profile/', headers={'Authorization': f'Token {token}'}) assert profile_resp.status_code == 200 assert profile_resp.json()['username'] == username ``` `f'pytest_{int(time.time())}'` генерує username на кшталт `pytest_1735689600` -- унікальний з точністю до секунди. Якщо тести виконуються паралельно, додайте також випадковий суфікс.

Довідникова картка: інтеграційне тестування

#
## Класифікація тестів | Тип | Реальна мережа? | Реальна БД? | Швидкість | Для чого | |---|---|---|---|---| | Юніт | Ні (замоковано) | Ні | Швидко | Логіка, алгоритми, трансформації | | Інтеграційний | Так | Так | Повільно | Контракти API, потоки автентифікації, пайплайни даних | | Наскрізний | Так | Так | Найповільніше | Повні користувацькі сценарії | ## Шаблон session-scoped фікстури автентифікації ```python # conftest.py import pytest import requests BASE_URL = 'https://apilearn.tukas.dev' @pytest.fixture(scope='session') def base_url(): return BASE_URL @pytest.fixture(scope='session') def auth_headers(base_url): resp = requests.post(f'{base_url}/api/auth/token/', json={ 'username': 'testuser', 'password': 'TestUser2024!', }) resp.raise_for_status() return {'Authorization': f'Token {resp.json()["token"]}'} ``` ## Поширені перевірки API ```python # Статус assert resp.status_code == 200 assert resp.status_code in (200, 201) # Структура JSON data = resp.json() assert 'results' in data assert isinstance(data['results'], list) # Пагінація assert len(data['results']) <= page_size # Наявність полів assert all('slug' in p for p in data['results']) # Потрібна автентифікація (має повернути редирект або 401 без токена) resp = requests.get(url) # без заголовків assert resp.status_code == 401 ``` ## Унікальні тестові дані (уникнення колізій) ```python import time, uuid username = f'test_{int(time.time())}' # унікальний з точністю до секунди username = f'test_{uuid.uuid4().hex[:8]}' # унікальний при кожному виклику ``` ## Позначення та відбір інтеграційних тестів ```ini # pytest.ini markers = integration: marks tests that require network access ``` ```python @pytest.mark.integration def test_something(): ... ``` ```bash pytest -m integration # лише інтеграційні pytest -m "not integration" # лише юніт-тести (швидкі) ``` ## Чекліст ізоляції тестів - [ ] Використовувати унікальні username/ID при створенні ресурсів - [ ] Видаляти створені ресурси у teardown фікстури - [ ] Використовувати session-scope для автентифікації (вхід один раз) - [ ] Перевіряти структуру та обмеження, а не точні значення, що можуть змінюватися - [ ] Обробляти і 200, і 201, де обидва є допустимими
01

Інтеграційний потік реєстрації та входу

#

Напишіть інтеграційний тест `test_register_and_login`, що: 1. Генерує унікальний username через `f'pytest_{int(time.time())}'` 2. Реєструє користувача через `POST /api/users/register/` з полями: `username`, `email`, `password`, `first_name`, `last_name` 3. Перевіряє, що відповідь реєстрації є 200 або 201 4. Виконує вхід через `POST /api/auth/token/` з новими обліковими даними 5. Виконує GET `/api/users/profile/` з поверненим токеном 6. Перевіряє, що `username` профілю збігається з зареєстрованим Використовуйте `BASE_URL = 'https://apilearn.tukas.dev'`.

# test_register_flow.py
import time
import requests

BASE_URL = 'https://apilearn.tukas.dev'


def test_register_and_login():
    username = f'pytest_{int(time.time())}'

    # Крок 1: реєстрація
    reg = requests.post(f'{BASE_URL}/api/users/register/', json={
        'username': username,
        'email': f'{username}@example.com',
        'password': ...,
        'first_name': 'Test',
        'last_name': 'User',
    })
    assert reg.status_code in (200, 201)

    # Крок 2: вхід
    auth = requests.post(f'{BASE_URL}/api/auth/token/', json={
        'username': username,
        'password': ...,
    })
    assert auth.status_code == 200
    token = ...

    # Крок 3: отримати профіль
    profile = requests.get(
        f'{BASE_URL}/api/users/profile/',
        headers={'Authorization': f'Token {token}'},
    )
    assert profile.status_code == 200
    assert ...
Рішення
# test_register_flow.py
import time
import requests

BASE_URL = 'https://apilearn.tukas.dev'


def test_register_and_login():
    username = f'pytest_{int(time.time())}'
    password = 'Secure123!'

    reg = requests.post(f'{BASE_URL}/api/users/register/', json={
        'username': username,
        'email': f'{username}@example.com',
        'password': password,
        'first_name': 'Test',
        'last_name': 'User',
    })
    assert reg.status_code in (200, 201)

    auth = requests.post(f'{BASE_URL}/api/auth/token/', json={
        'username': username,
        'password': password,
    })
    assert auth.status_code == 200
    token = auth.json()['token']

    profile = requests.get(
        f'{BASE_URL}/api/users/profile/',
        headers={'Authorization': f'Token {token}'},
    )
    assert profile.status_code == 200
    assert profile.json()['username'] == username
02

Тестування обмежень пагінації

#

Напишіть session-scoped фікстуру `base_url`, що повертає `'https://apilearn.tukas.dev'`, потім напишіть три тести: 1. `test_products_returns_results` -- GET `/api/products/`, перевірте статус 200, наявність ключа `'results'`, що результат є списком з принаймні 1 елементом 2. `test_page_size_two` -- GET `/api/products/?page_size=2`, перевірте, що список results має **не більше** 2 елементів 3. `test_second_page` -- GET `/api/products/?page=2&page_size=2`, перевірте статус 200 (сторінка 2 може бути порожньою, але не має повертати 404)

# conftest.py
import pytest


@pytest.fixture(scope='session')
def base_url():
    return 'https://apilearn.tukas.dev'


# test_pagination.py
import requests


def test_products_returns_results(base_url):
    resp = requests.get(f'{base_url}/api/products/')
    ...


def test_page_size_two(base_url):
    resp = requests.get(f'{base_url}/api/products/?page_size=2')
    ...


def test_second_page(base_url):
    resp = requests.get(f'{base_url}/api/products/?page=2&page_size=2')
    ...
Рішення
# conftest.py
import pytest


@pytest.fixture(scope='session')
def base_url():
    return 'https://apilearn.tukas.dev'


# test_pagination.py
import requests


def test_products_returns_results(base_url):
    resp = requests.get(f'{base_url}/api/products/')
    assert resp.status_code == 200
    data = resp.json()
    assert 'results' in data
    assert isinstance(data['results'], list)
    assert len(data['results']) > 0


def test_page_size_two(base_url):
    resp = requests.get(f'{base_url}/api/products/?page_size=2')
    assert resp.status_code == 200
    results = resp.json()['results']
    assert len(results) <= 2


def test_second_page(base_url):
    resp = requests.get(f'{base_url}/api/products/?page=2&page_size=2')
    assert resp.status_code == 200
03

Session-scoped автентифікація для кількох тестів

#

Напишіть `conftest.py` з двома session-scoped фікстурами: - `base_url` -- повертає `'https://apilearn.tukas.dev'` - `auth_headers` -- виконує вхід як `testuser` / `TestUser2024!` і повертає `{'Authorization': 'Token ...'}` Напишіть три тести, кожен з яких використовує `auth_headers`: 1. `test_profile_username` -- перевірте, що відповідь профілю має `username == 'testuser'` 2. `test_products_with_auth` -- перевірте, що GET `/api/products/` повертає 200 (автентифікований) 3. `test_echo_with_auth` -- перевірте, що GET `/api/echo/` повертає 200

# conftest.py
import pytest
import requests


@pytest.fixture(scope='session')
def base_url():
    return 'https://apilearn.tukas.dev'


@pytest.fixture(scope='session')
def auth_headers(base_url):
    resp = requests.post(f'{base_url}/api/auth/token/', json={
        'username': 'testuser',
        'password': ...,
    })
    resp.raise_for_status()
    token = ...
    return {'Authorization': f'Token {token}'}


# test_authenticated.py
import requests


def test_profile_username(base_url, auth_headers):
    ...


def test_products_with_auth(base_url, auth_headers):
    ...


def test_echo_with_auth(base_url, auth_headers):
    ...
Рішення
# conftest.py
import pytest
import requests


@pytest.fixture(scope='session')
def base_url():
    return 'https://apilearn.tukas.dev'


@pytest.fixture(scope='session')
def auth_headers(base_url):
    resp = requests.post(f'{base_url}/api/auth/token/', json={
        'username': 'testuser',
        'password': 'TestUser2024!',
    })
    resp.raise_for_status()
    token = resp.json()['token']
    return {'Authorization': f'Token {token}'}


# test_authenticated.py
import requests


def test_profile_username(base_url, auth_headers):
    resp = requests.get(f'{base_url}/api/users/profile/', headers=auth_headers)
    assert resp.status_code == 200
    assert resp.json()['username'] == 'testuser'


def test_products_with_auth(base_url, auth_headers):
    resp = requests.get(f'{base_url}/api/products/', headers=auth_headers)
    assert resp.status_code == 200


def test_echo_with_auth(base_url, auth_headers):
    resp = requests.get(f'{base_url}/api/echo/', headers=auth_headers)
    assert resp.status_code == 200
04

Та сама функція: замокована vs реальна HTTP

#

Реалізуйте `get_product_count(base_url)` -- функцію, що виконує GET `{base_url}/api/products/` і повертає ціле значення поля `count` у відповіді JSON. Напишіть два тести: 1. `test_get_product_count_unit` -- використайте `unittest.mock.patch` для мокування `requests.get`, змусьте його повертати фейкову JSON-відповідь `{'count': 7, 'results': []}` і перевірте, що `get_product_count(...)` повертає `7`. Тест **не повинен** виконувати реальний мережевий виклик. 2. `test_get_product_count_integration` -- викличте `get_product_count('https://apilearn.tukas.dev')` реально, перевірте, що результат є цілим числом >= 0. Додайте коментар у кожному тесті, що пояснює, що він може і не може виявити.

# test_count_comparison.py
import requests
from unittest.mock import patch


def get_product_count(base_url):
    resp = requests.get(f'{base_url}/api/products/')
    resp.raise_for_status()
    return resp.json()['count']


def test_get_product_count_unit():
    # Цей тест: швидкий, без мережі.
    # Не може виявити: поле перейменоване в реальному API, неправильний URL, проблеми з автентифікацією.
    with patch('requests.get') as mock_get:
        mock_get.return_value.status_code = 200
        mock_get.return_value.raise_for_status.return_value = None
        mock_get.return_value.json.return_value = ...
        result = get_product_count('http://fake')
    assert result == ...


def test_get_product_count_integration():
    # Цей тест: повільніший, реальна мережа.
    # Не може виявити: баги логіки всередині get_product_count (покривається юніт-тестом).
    result = get_product_count('https://apilearn.tukas.dev')
    assert ...
Рішення
# test_count_comparison.py
import requests
from unittest.mock import patch


def get_product_count(base_url):
    resp = requests.get(f'{base_url}/api/products/')
    resp.raise_for_status()
    return resp.json()['count']


def test_get_product_count_unit():
    # Цей тест: швидкий, без мережі.
    # Не може виявити: поле перейменоване в реальному API, неправильний URL, проблеми з автентифікацією.
    with patch('requests.get') as mock_get:
        mock_get.return_value.status_code = 200
        mock_get.return_value.raise_for_status.return_value = None
        mock_get.return_value.json.return_value = {'count': 7, 'results': []}
        result = get_product_count('http://fake')
    assert result == 7


def test_get_product_count_integration():
    # Цей тест: повільніший, реальна мережа.
    # Не може виявити: баги логіки всередині get_product_count (покривається юніт-тестом).
    result = get_product_count('https://apilearn.tukas.dev')
    assert isinstance(result, int)
    assert result >= 0
05

Session-scoped фікстура, що використовується в трьох незалежних тестах

#

Побудуйте session-scoped фікстуру `product_list`, що: - Виконує GET `https://apilearn.tukas.dev/api/products/` - Перевіряє статус 200 - Повертає розібраний список `results` Напишіть три незалежні тести, кожен з яких отримує `product_list` як параметр фікстури: 1. `test_products_not_empty` -- перевірте, що список має принаймні 1 елемент 2. `test_products_have_slug` -- перевірте, що кожен продукт має непорожнє поле `slug` 3. `test_products_have_name` -- перевірте, що кожен продукт має непорожнє поле `name` HTTP-виклик має відбутися **один раз** незалежно від кількості тестів, що використовують фікстуру.

# conftest.py
import pytest
import requests


@pytest.fixture(scope='session')
def product_list():
    resp = requests.get('https://apilearn.tukas.dev/api/products/')
    assert resp.status_code == 200
    return resp.json()['results']


# test_product_list.py


def test_products_not_empty(product_list):
    assert len(product_list) > 0


def test_products_have_slug(product_list):
    ...


def test_products_have_name(product_list):
    ...
Рішення
# conftest.py
import pytest
import requests


@pytest.fixture(scope='session')
def product_list():
    resp = requests.get('https://apilearn.tukas.dev/api/products/')
    assert resp.status_code == 200
    return resp.json()['results']


# test_product_list.py


def test_products_not_empty(product_list):
    assert len(product_list) > 0


def test_products_have_slug(product_list):
    assert all(isinstance(p.get('slug'), str) and len(p['slug']) > 0
               for p in product_list)


def test_products_have_name(product_list):
    assert all(isinstance(p.get('name'), str) and len(p['name']) > 0
               for p in product_list)