Перейти к содержимому
К программе курса
Pytest: Борьба с Race Conditions в Async-коде
1 / 911%

Фундамент: pytest-asyncio и окружение

60 минут

Этот урок — критический фундамент всего курса. Мы потратим 60 минут (30 из них на установку!) чтобы настроить полноценное async-окружение для тестирования.

ВАЖНО: Без правильной настройки все следующие уроки будут бессмысленны. Не пропускайте этот урок.

Prerequisite Check

Перед началом убедитесь:

# test_async_knowledge.py
import asyncio
 
async def check_knowledge():
    """Вы ДОЛЖНЫ понимать этот код"""
    tasks = [asyncio.sleep(0.1) for _ in range(5)]
    await asyncio.gather(*tasks)
 
    lock = asyncio.Lock()
    async with lock:
        print("Critical section")
 
check_knowledge()  # Если не понимаете — курс НЕ для вас!

Если этот код для вас непонятен — вернитесь к изучению async/await. Без этого фундамента курс будет потерей времени.

Установка окружения (30 минут)

Шаг 1: Docker (10 минут)

macOS:

# Установите Docker Desktop
brew install --cask docker
# Запустите Docker Desktop из Applications

Linux (Ubuntu):

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Перелогиньтесь!

Windows:

  • Скачайте Docker Desktop с docker.com
  • Включите WSL 2

Проверка:

docker --version
# Docker version 24.0.0 или выше

Шаг 2: PostgreSQL (10 минут)

docker run -d \
  --name pytest-postgres \
  -e POSTGRES_PASSWORD=testpass \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_DB=pytest_test \
  -p 5432:5432 \
  postgres:15-alpine
 
# Проверка
docker exec pytest-postgres pg_isready
# /var/run/postgresql:5432 - accepting connections

Создаём тестовую схему:

docker exec -i pytest-postgres psql -U postgres -d pytest_test <<EOF
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);
EOF

Шаг 3: Redis (5 минут)

docker run -d \
  --name pytest-redis \
  -p 6379:6379 \
  redis:7-alpine
 
# Проверка
docker exec pytest-redis redis-cli ping
# PONG

Шаг 4: Python зависимости (5 минут)

# Создайте виртуальное окружение
python3 -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
 
# Установите зависимости
pip install pytest pytest-asyncio asyncpg redis[asyncio]

Проверка установки:

# test_deps.py
import pytest
import asyncpg
import redis.asyncio as aioredis
 
print("✅ All deps installed")
python test_deps.py
# ✅ All deps installed

pytest-asyncio: первый тест (15 минут)

Настройка pytest.ini

# pytest.ini
[pytest]
asyncio_mode = auto

Что делает asyncio_mode = auto:

  • Автоматически обнаруживает async тесты
  • Управляет event loop за вас
  • Не нужно декорировать каждый тест

Первый async тест

# tests/test_first_async.py
import pytest
 
@pytest.mark.asyncio
async def test_simple_async():
    """Простейший async тест"""
    import asyncio
    await asyncio.sleep(0.001)
    assert True

Запускаем:

pytest tests/test_first_async.py -v

Ожидаемый результат:

test_simple_async PASSED

✅ Если тест прошел — pytest-asyncio работает!

Async фикстуры: базовый пример (15 минут)

Простая async фикстура

# conftest.py
import pytest
 
@pytest.fixture
async def async_value():
    """Async фикстура возвращает значение"""
    import asyncio
    await asyncio.sleep(0.001)  # Имитация async операции
    return 42
 
@pytest.mark.asyncio
async def test_async_fixture(async_value):
    """Тест использует async фикстуру"""
    assert async_value == 42

Фикстура с setup/teardown

# conftest.py
import pytest
 
@pytest.fixture
async def async_resource():
    """Async фикстура с teardown"""
    # Setup
    print("Opening async resource")
    resource = {"status": "open"}
 
    yield resource  # Передаём в тест
 
    # Teardown
    print("Closing async resource")
    resource["status"] = "closed"

Тест:

@pytest.mark.asyncio
async def test_resource(async_resource):
    assert async_resource["status"] == "open"
    # После теста выполнится teardown

Подключение к PostgreSQL (10 минут)

Async фикстура для PostgreSQL

# conftest.py
import pytest
import asyncpg
 
@pytest.fixture
async def db_connection():
    """Async connection к PostgreSQL"""
    conn = await asyncpg.connect(
        host="localhost",
        port=5432,
        user="postgres",
        password="testpass",
        database="pytest_test"
    )
 
    yield conn
 
    await conn.close()

Первый тест с БД

# tests/test_postgres.py
import pytest
 
@pytest.mark.asyncio
async def test_postgres_connection(db_connection):
    """Проверяем что БД доступна"""
    result = await db_connection.fetchval("SELECT 1")
    assert result == 1
 
@pytest.mark.asyncio
async def test_insert_user(db_connection):
    """Вставляем пользователя"""
    await db_connection.execute(
        "INSERT INTO users (email) VALUES ($1)",
        "test@example.com"
    )
 
    count = await db_connection.fetchval("SELECT COUNT(*) FROM users")
    assert count > 0

Запускаем:

pytest tests/test_postgres.py -v

Проблема: Второй запуск упадет! Почему?

asyncpg.UniqueViolationError: duplicate key value

Причина: Данные остались в БД после первого теста.

Исправление (в следующем уроке): добавим транзакции с rollback.

Подключение к Redis (10 минут)

Async фикстура для Redis

# conftest.py
import pytest
import redis.asyncio as aioredis
 
@pytest.fixture
async def redis_client():
    """Async Redis client"""
    client = aioredis.from_url(
        "redis://localhost:6379/15",  # DB 15 для тестов
        decode_responses=True
    )
 
    yield client
 
    await client.flushdb()  # Чистим после теста
    await client.close()

Первый тест с Redis

# tests/test_redis.py
import pytest
 
@pytest.mark.asyncio
async def test_redis_set_get(redis_client):
    """Set/Get в Redis"""
    await redis_client.set("key", "value")
    result = await redis_client.get("key")
    assert result == "value"
 
@pytest.mark.asyncio
async def test_redis_isolation(redis_client):
    """Проверяем изоляцию"""
    # Ключ из предыдущего теста не должен быть виден
    result = await redis_client.get("key")
    assert result is None  # ✅ flushdb сработал

Типичные ошибки окружения

Ошибка #1: Забыли pytest.ini

# ❌ БЕЗ pytest.ini
@pytest.mark.asyncio  # Нужен декоратор!
async def test():
    pass
# ✅ С pytest.ini
[pytest]
asyncio_mode = auto
# Декоратор не нужен для async def test_*

Ошибка #2: Неправильный asyncio_mode

# ❌ ПЛОХО
asyncio_mode = strict  # Старый режим
 
# ✅ ХОРОШО
asyncio_mode = auto  # Современный

Ошибка #3: Docker контейнеры не запущены

# Проверка
docker ps
 
# Если контейнеров нет:
docker start pytest-postgres pytest-redis

Ошибка #4: Порты заняты

# Проверка портов
lsof -i :5432  # PostgreSQL
lsof -i :6379  # Redis
 
# Если порт занят, остановите процесс или измените порт
docker run -p 5433:5432 postgres:15-alpine

Проверка финального окружения

# tests/test_environment.py
import pytest
import asyncpg
import redis.asyncio as aioredis
 
@pytest.mark.asyncio
async def test_full_environment():
    """Проверяем что всё работает"""
    # PostgreSQL
    pg_conn = await asyncpg.connect(
        "postgresql://postgres:testpass@localhost:5432/pytest_test"
    )
    pg_result = await pg_conn.fetchval("SELECT 1")
    await pg_conn.close()
    assert pg_result == 1
 
    # Redis
    redis = aioredis.from_url("redis://localhost:6379/15")
    await redis.set("test", "ok")
    redis_result = await redis.get("test")
    await redis.close()
    assert redis_result == "ok"
 
    print("✅ Environment ready!")

Запускаем:

pytest tests/test_environment.py -v -s

Ожидаемый результат:

test_full_environment PASSED
✅ Environment ready!

Что вы настроили

Docker с PostgreSQL и Redis ✅ pytest-asyncio с auto mode ✅ asyncpg для async PostgreSQL ✅ redis[asyncio] для async Redis ✅ Базовые async фикстуры

Следующий урок

Теперь окружение готово. В следующем уроке добавим транзакции с rollback для полной изоляции тестов.

Переходите к уроку 1: Async фикстуры для PostgreSQL

Troubleshooting

Проблема: docker: command not found Решение: Установите Docker (см. Шаг 1)

Проблема: asyncpg.CannotConnectNowError Решение: PostgreSQL контейнер не запущен: docker start pytest-postgres

Проблема: ModuleNotFoundError: No module named 'pytest_asyncio' Решение: pip install pytest-asyncio

Проблема: Тесты висят и не завершаются Решение: Проверьте pytest.ini, должно быть asyncio_mode = auto

Фундамент: pytest-asyncio и окружение — Pytest: Борьба с Race Conditions в Async-коде — Potapov.me