CI и автозапуск тестов

Как сделать так, чтобы тесты запускались сами на каждое изменение и не пускали в проект сломанный код.

Непрерывная интеграция (CI) — практика, при которой каждое изменение автоматически собирается и прогоняется через тесты на сервере, прежде чем попасть в основную ветку.

Зачем CI

«У меня локально всё работает» — частая причина сломанных сборок: у коллеги другая версия, забытый файл, иное окружение. CI прогоняет тесты в чистом, одинаковом для всех окружении на сервере. Если тесты красные — изменение не вливается. Так основная ветка всегда остаётся рабочей.

Типичный пайплайн

  1. Разработчик делает git push или открывает pull request.
  2. CI-сервер поднимает чистое окружение и ставит зависимости.
  3. Запускает линтер и тесты.
  4. Зелено — разрешает слияние; красно — блокирует и сообщает автору.

Такой автозапуск умеют GitHub Actions, GitLab CI, Jenkins и другие. На GitHub пайплайн описывают YAML-файлом в .github/workflows/. Это конфиг, не запускаемый код:

name: tests
on: [push, pull_request]      # запускать на каждый push и PR
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest            # упадёт -> сборка красная -> merge блокируется

Команда запуска тестов в проекте — обычно одна строка:

# локально и в CI запускается одинаково
pytest -q

Что именно проверяет CI

Сами тесты — обычный код с проверками, который мы писали весь курс. CI лишь запускает их автоматически. Смоделируем, что делает CI-шаг «прогнать тесты»: собирает результаты и решает, «зелено» или «красно».

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

# Набор тестов проекта
def test_add_positive(): assert add(2, 3) == 5
def test_add_zero():     assert add(5, 0) == 5
def test_add_negative(): assert add(-1, -1) == -2

# Имитация CI: прогнать все тесты и вынести вердикт
suite = [test_add_positive, test_add_zero, test_add_negative]
failed = 0
for t in suite:
    try:
        t()
    except AssertionError:
        failed += 1

if failed == 0:
    print(f"CI: PASS — {len(suite)} тестов зелёные, слияние разрешено")
else:
    print(f"CI: FAIL — {failed} тест(ов) упало, merge заблокирован")

Вывод:

CI: PASS — 3 тестов зелёные, слияние разрешено

Итог

  • CI автоматически прогоняет тесты в чистом окружении на каждое изменение.
  • Красные тесты блокируют слияние — основная ветка всегда рабочая.
  • На GitHub пайплайн описывают YAML в .github/workflows/; тесты запускает одна команда.
Проверьте себя
1. Что делает непрерывная интеграция (CI)?
AПишет тесты за разработчика
BАвтоматически собирает проект и прогоняет тесты на каждое изменение в чистом окружении
CУдаляет старый код
DЗаменяет систему контроля версий
2. Почему «у меня локально работает» — ненадёжно без CI?
AЛокальная машина быстрее
BУ коллег другое окружение, версии и забытые файлы; CI проверяет в одинаковом чистом окружении
CЛокально тесты не запускаются
DCI пишет код вместо разработчика
3. Где на GitHub описывают пайплайн запуска тестов?
AВ README.md
BВ YAML-файле в .github/workflows/
CВ самом коде тестов
DВ коммит-сообщении
Поддержать проект