CI/CD6 мин чтения2026-03-06

Автоматизация тестирования на Python: от unit-тестов к полному CI/CD

Когда я работал в Яндексе, мы писали тесты вручную, запускали их перед каждым коммитом и половину времени забывали. Результат? Боги знают сколько багов прошло в

Когда я работал в Яндексе, мы писали тесты вручную, запускали их перед каждым коммитом и половину времени забывали. Результат? Боги знают сколько багов прошло в production. Потом мы автоматизировали процесс — и всё изменилось. Сейчас автоматизация тестирования на Python это не роскошь, а базовый стандарт. Если ты ещё не внедрил это в свой workflow, то честно — отстаёшь.

В этой статье я расскажу, как организовать полный цикл автоматизированного тестирования, какие инструменты использовать, и как интегрировать всё в CI/CD pipeline. Не будет общих фраз — только то, что работает на боевых проектах.

Почему автоматизация тестирования — это не опционально

Короче, давайте от простого. Если твоя команда:

...то вы теряете недели времени на рутину. На одном проекте мы считали: ручной запуск тестов и их анализ занимал 3-4 часа в день на каждого разработчика. Это просто деньги на ветер.

Автоматизация означает:

По статистике, автоматизация тестирования сокращает время на релиз в 5-10 раз. И это не маркетинг — это реальные цифры из наших проектов.

Три уровня тестирования и инструменты

Прежде чем писать pipeline, разберёмся с типами тестов. Классическая пирамида:

Unit-тесты — тестируем отдельные функции. Быстро. Пишет разработчик.

import pytest

def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

Инструмент: pytest. Это король среди фреймворков для Python. Почему? Потому что он просто работает. Нет боли, нет конфигурации, нет магии.

Integration-тесты — проверяем, как компоненты работают вместе. Медленнее unit-тестов, но всё ещё быстро.

import pytest
from fastapi.testclient import TestClient
from app import app

client = TestClient(app)

def test_api_create_user():
    response = client.post("/users/", json={"name": "John", "email": "john@example.com"})
    assert response.status_code == 201
    assert response.json()["name"] == "John"

Для API используем requests или встроенный TestClient из FastAPI. Для БД — SQLAlchemy с тестовой базой в памяти.

E2E-тесты — проверяем весь пользовательский сценарий в боевом окружении. Медленно, но критично важно.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def test_user_registration_flow():
    driver = webdriver.Chrome()
    driver.get("https://app.example.com")
    
    # Кликаем на "Зарегистрироваться"
    register_btn = driver.find_element(By.ID, "register-btn")
    register_btn.click()
    
    # Заполняем форму
    email_field = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.NAME, "email"))
    )
    email_field.send_keys("test@example.com")
    
    password_field = driver.find_element(By.NAME, "password")
    password_field.send_keys("SecurePass123!")
    
    driver.find_element(By.ID, "submit-btn").click()
    
    # Проверяем, что попали на страницу профиля
    assert "profile" in driver.current_url
    driver.quit()

Инструмент: Selenium для браузерной автоматизации. Или Playwright — он быстрее и стабильнее, если честно.

По-хорошему, должно быть примерно так:

Настройка CI/CD pipeline с GitHub Actions

Теперь переходим к интересному. Как встроить все эти тесты в процесс разработки?

Начнём с самого популярного варианта — GitHub Actions. Это бесплатно, встроено в GitHub, и работает из коробки.

Создаём файл .github/workflows/tests.yml:

name: Tests & Code Quality

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        python-version: ["3.9", "3.10", "3.11"]
    
    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Cache pip packages
      uses: actions/cache@v3
      with:
        path: ~/.cache/pip
        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
        restore-keys: |
          ${{ runner.os }}-pip-
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pytest pytest-cov pytest-xdist
    
    - name: Run unit tests
      run: |
        pytest tests/unit/ -v --cov=src --cov-report=xml
    
    - name: Run integration tests
      env:
        DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
      run: |
        pytest tests/integration/ -v
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage.xml
        fail_ci_if_error: true

Что здесь происходит:

  1. Триггер: запускается на каждый push в main/develop и на каждый pull request
  2. Матрица: тестируем на Python 3.9, 3.10, 3.11 — ловим проблемы совместимости
  3. Сервис Postgres: для интеграционных тестов нужна настоящая база. GitHub Actions поднимает её автоматически
  4. Кэширование: pip-пакеты кэшируются между запусками — экономим 2-3 минуты
  5. Параллельный запуск: pytest-xdist распределяет тесты между ядрами — быстрее в 4 раза
  6. Coverage: отслеживаем покрытие кода тестами

Запустится — и за 5-10 минут ты узнаешь, сломал ли что-то. Если тесты упали, PR не сможет смержиться. Работает.

GitLab CI — для более сложных сценариев

Если ты на GitLab (например, на своих серверах в РФ), то там ещё более гибко.

.gitlab-ci.yml:

stages:
  - test
  - build
  - deploy

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  paths:
    - .cache/pip
    - venv/

before_script:
  - python -V
  - pip install virtualenv
  - virtualenv venv
  - source venv/bin/activate
  - pip install -r requirements.txt

unit_tests:
  stage: test
  image: python:3.11
  script:
    - pytest tests/unit/ -v --cov=src --cov-report=term --cov-report=html
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    paths:
      - htmlcov/
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

integration_tests:
  stage: test
  image: python:3.11
  services:
    - postgres:14
  variables:
    POSTGRES_DB: test_db
    POSTGRES_USER: postgres
    POSTGRES_PASSWORD: postgres
    DATABASE_URL: postgresql://postgres:postgres@postgres:5432/test_db
  script:
    - pytest tests/integration/ -v --tb=short

e2e_tests:
  stage: test
  image: python:3.11
  services:
    - selenium/standalone-chrome:latest
  script:
    - pip install selenium
    - pytest tests/e2e/ -v --tb=short
  only:
    - main
    - tags

lint:
  stage: test
  image: python:3.11
  script:
    - pip install flake8 black isort mypy
    - black --check .
    - isort --check-only .
    - flake8 . --max-line-length=120
    - mypy src/

GitLab позволяет:

Автоматизация тестирования API с помощью Python

Отдельно про API-тестирование, потому что это большой класс задач.

Если у тебя REST API, то вот простой, но эффективный подход:

import pytest
import requests
from typing import Dict, Any

BASE_URL = "http://localhost:8000"

class TestUserAPI:
    @pytest.fixture(autouse=True)
    def setup(self):
        # Очищаем БД перед каждым тестом
        requests.delete(f"{BASE_URL}/admin/reset")
        yield
    
    def test_create_user(self):
        payload = {
            "username": "alice",
            "email": "alice@example.com",
            "password": "SecurePass123!"
        }
        response = requests.post(f"{BASE_URL}/users", json=payload)
        
        assert response.status_code == 201
        data = response.json()
        assert data["username"] == "alice"
        assert data["id"] > 0
    
    def test_get_user(self):
        # Сначала создаём
        requests.post(f"{BASE_URL}/users", json={
            "username": "bob",
            "email": "bob@example.com",
            "password": "Pass123!"
        })
        
        # Потом получаем
        response = requests.get(f"{BASE_URL}/users/bob")
        assert response.status_code == 200
        assert response.json()["email"] == "bob@example.com"
    
    def test_invalid_email_returns_400(self):
        response = requests.post(f"{BASE_URL}/users", json={
            "username": "charlie",
            "email": "not-an-email",
            "password": "Pass123!"
        })
        assert response.status_code == 400
        assert "email" in response.json()["errors"]
    
    @pytest.mark.parametrize("password", ["123", "pass", "p@ss"])
    def test_weak_password_validation(self, password):
        response = requests.post(f"{BASE_URL}/users", json={
            "username": "david",
            "email": "david@example.com",
            "password": password
        })
        assert response.status_code == 400

Ключ здесь — pytest.mark.parametrize. Вместо того, чтобы писать 3 отдельных теста, пишешь один и параметризуешь его. Код меньше, читается лучше, поддерживается проще.

Для более сложных сценариев можно использовать pytest-asyncio (если API асинхронное) или httpx вместо requests (асинхронные клиент).

Selenium и Playwright: браузерная автоматизация

Selenium — классика, но медленная. На одном проекте мы перешли с Selenium на Playwright и выиграли в скорости в 2-3 раза.

Вот сравнение:

# Selenium (медленно)
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example.com")
element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.ID, "submit"))
)
element.click()
# Playwright (быстро и удобно)
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")
    page.click("#submit")
    browser.close()

Playwright проще, быстрее, и лучше обрабатывает асинхронность. Но Selenium всё ещё стандарт в индустрии, если ты на собеседовании упомянешь Selenium — это плюс.

Для CI/CD обычно E2E-тесты запускаются только на main-ветке или перед релизом. Они медленные, и запускать их на каждый коммит — это трата ресурсов.

Линтинг и про

Попробуйте Distiq для автоматического code review

AI-бот анализирует каждый MR/PR и оставляет комментарии с замечаниями. Интеграция за 2 минуты.

Попробовать бесплатно

Похожие статьи