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

Интеграционное тестирование

5 задач

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

Юнит vs интеграционные vs end-to-end тесты: когда использовать каждый

#
## Пирамида тестирования Не все тесты служат одной цели. Здоровый тест-сьют использует три разных уровня: ``` +-------------------+ | End-to-End | немного, медленные, тестируют систему целиком +-------------------+ | Интеграционные | больше, тестируют реальные компоненты вместе +-------------------+ | Юнит | много, быстрые, тестируют одно в изоляции +-------------------+ ``` В большинстве проектов должно быть много юнит-тестов, умеренное количество интеграционных и небольшое число 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), могут быть нестабильными (сетевые или серверные проблемы), требуют реального сервера, могут оставлять данные. ## End-to-end тесты: вся система End-to-end тест управляет всей системой через её реальный интерфейс -- обычно браузер для веб-приложений или полную поверхность CLI/API. Он проверяет, что все уровни (фронтенд, бэкенд, база данных) работают вместе. End-to-end тесты самые дорогие: медленные, хрупкие, сложные в отладке. Используйте их экономно -- только для самых критичных пользовательских сценариев. ## Что делает тест интеграционным? Тест является интеграционным, если он: - Выполняет реальные сетевые вызовы (HTTP, gRPC, WebSocket) - Читает или пишет в реальную базу данных - Читает или пишет реальные файлы на диске - Использует реальные внешние сервисы (провайдеры аутентификации, платёжные шлюзы, очереди) Если тест использует моки, он является юнит-тестом независимо от того, сколько функций вызывает внутри. ## Затраты интеграционных тестов **Медленные:** сетевой round-trip занимает 50-500 мс. Юнит-тест занимает 0.1 мс. Сто интеграционных тестов занимают 5-50 секунд; сто юнит-тестов -- 0.01 секунды. **Нестабильные:** сетевые сбои, таймауты, ограничения по частоте запросов и перезапуски серверов приводят к тому, что интеграционные тесты падают по причинам, не связанным с вашим кодом. Нестабильный тест-сьют подрывает доверие к CI. **Загрязнение состояния:** интеграционные тесты могут создавать данные, влияющие на последующие тесты. Тест, создающий пользователя, но не удаляющий его, может привести к падению другого теста, проверяющего "нет пользователей с этим именем". **Зависимости:** интеграционные тесты требуют запущенного реального сервера. В CI это означает развёртывание тестовой среды, ожидание её готовности, а затем запуск тестов. ## Стратегии изоляции тестов **Используйте уникальные идентификаторы:** при создании пользователей или ресурсов генерируйте уникальные значения (временные метки, UUID), чтобы тесты не конфликтовали между собой или с уже существующими данными: ```python import time def test_register_and_login(): username = f'testuser_{int(time.time())}' ... ``` **Убирайте за собой:** если тест создаёт ресурс, удалите его в конце. Используйте для этого завершение фикстуры: ```python @pytest.fixture def temp_user(): username = f'tmp_{int(time.time())}' # настройка: создать пользователя resp = requests.post(BASE_URL + '/api/users/register/', json={...}) yield {'username': username, 'token': get_token(username)} # завершение: удалить пользователя requests.delete(BASE_URL + f'/api/users/{username}/', headers=auth_headers) ``` **Используйте выделенный тестовый аккаунт:** для тестов, читающих данные (не пишущих), аутентифицируйтесь через известный тестовый аккаунт, а не создавайте новых пользователей. Аккаунт `testuser` / `TestUser2024!` на `apilearn.tukas.dev` подходит для этого. **Аутентификация на уровне сессии:** если многим тестам нужна аутентификация, выполните вход один раз с фикстурой уровня session и разделите токен. Это сокращает накладные расходы на вход с 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 Вход в систему для каждого теста медленный и расточительный. Фикстура уровня session выполняет вход один раз: ```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 здесь уместен, поскольку токен не меняется между тестами. ## Пример 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: Юнит-тест против интеграционного -- рядом Одна и та же функция `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 = 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())}'` генерирует имя пользователя вида `pytest_1735689600` -- уникальное с точностью до секунды. Если тесты запускаются параллельно, добавьте случайный суффикс для гарантированной глобальной уникальности.

Справочная карточка: интеграционное тестирование

#
## Классификация тестов | Тип | Реальная сеть? | Реальная БД? | Скорость | Применение | |---|---|---|---|---| | Юнит | Нет (мок) | Нет | Быстрый | Логика, алгоритмы, преобразования | | Интеграционный | Да | Да | Медленный | Контракты API, потоки аутентификации, пайплайны данных | | End-to-end | Да | Да | Самый медленный | Полные пользовательские сценарии | ## Паттерн фикстуры аутентификации уровня session ```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" # только юнит (быстрые) ``` ## Контрольный список изоляции тестов - [ ] Использовать уникальные имена/ID при создании ресурсов - [ ] Удалять созданные ресурсы в завершении фикстуры - [ ] Использовать уровень session для аутентификации (вход один раз) - [ ] Проверять структуру и ограничения, а не точные значения, которые могут меняться - [ ] Принимать как 200, так и 201 там, где оба допустимы
01

Интеграционный поток регистрации и входа

#

Напишите интеграционный тест `test_register_and_login`, который: 1. Генерирует уникальное имя пользователя через `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. Получает `/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

Тестирование ограничений пагинации

#

Напишите фикстуру `base_url` уровня session, возвращающую `'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`, проверить что список результатов содержит **не более** 2 элементов 3. `test_second_page` -- GET `/api/products/?page=2&page_size=2`, проверить статус 200 (вторая страница может быть пустой, но не должна возвращать 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 для нескольких тестов

#

Напишите `conftest.py` с двумя фикстурами уровня session: - `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, используемая тремя независимыми тестами

#

Создайте фикстуру `product_list` уровня session, которая: - Делает 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)