Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 20 из 25 ~18 мин +50 XP

CI: тесты в GitHub Actions

Локально тесты зелёные, а на CI красные или зависают: знакомая развилка, у которой есть системное решение. Этот урок собирает минимальный workflow для GitHub Actions: джоба на раннере с готовым Docker, кэш модулей и образов, переменные окружения RYUK и TESTCONTAINERS, дисциплина таймаутов против флейков. Сценарий «testcontainers github actions» здесь закрыт целиком: от checkout до уборки за убитой джобой, а связка «тесты ci go» разобрана на уровне конкретных настроек, а не общих слов. Строить инфраструктуру не придётся: testcontainers приносит в CI те же гарантии, что и на ноутбуке, — реальный сервис поднимается, отвечает и убирается. Меняется только окружение: чистая машина, ограниченный бюджет времени, медленная сеть и демон, который вы разделяете с остальными прогонами. К концу урока у вас будет workflow, который проходит на пустом раннере, держит бюджет и не оставляет за собой мусора.

Содержание урока · 10 разделов

Интеграционные тесты с testcontainers на CI не требуют особого пайплайна: им нужен лишь демон Docker и честный бюджет времени. Всё остальное — настройки окружения, кэши, таймауты, уборка — расставляется в workflow и переменных окружения, не трогая код тестов. Этот урок — карта таких настроек: что уже установлено на hosted-раннере, какие env-переменные читает библиотека, где выигрывать минуты на pull'е образов и почему уборщика нельзя глушить в CI. Каждый раздел заканчивается решением конкретной проблемы, а сводная таблица в середине заменяет чат с коллегой «а как у вас в CI?».

Runner с Docker: что уже есть на ubuntu-latest#

Хостед-раннер ubuntu-latest — виртуалка, где Docker уже установлен: демон запущен, CLI на месте, сокет /var/run/docker.sock доступен, — поэтому testcontainers подключается к нему без единой настройки, тем же провайдером, что и на вашей машине. Джоба — чистая машина: каждый прогон начинается с пустого состояния, зомби от прошлых прогонов тут не бывают, зато каждый pull идёт из сети. Self-hosted раннер устроен наоборот: демон один на все джобы, контейнеры и слои копятся, порты разбираются, диск забивается. Там дисциплина уборки из урока про Ryuk перестаёт быть страховкой и становится требованием: уборщика не глушить, прогоны помечать, остатки периодически вычищать по меткам org.testcontainers. Конфигурация библиотеки при этом везде одна: сначала читается файл ~/.testcontainers.properties, затем поверх перекрывается переменными окружения, — так окружение CI настраивается прямо в workflow, без правок кода. Отсюда и весь рецепт связки «testcontainers github actions»: три шага в workflow плюс аккуратный env-блок, а «тесты ci go» ведут себя одинаково у всех в команде.

Отдельно о том, как библиотека находит демон на раннере: порядок детекции фиксирован. Сначала свойство tc.host из ~/.testcontainers.properties, затем переменная DOCKER_HOST, затем значение из контекста Go — так библиотека передаёт хост уборщику, — затем путь по умолчанию /var/run/docker.sock, затем свойство docker.host из файла и, наконец, пути rootless-сокетов. Не нашлось ничего — паника с сообщением, что Docker-хост не обнаружен. На hosted-раннере срабатывает шаг по умолчанию: демон слушает стандартный сокет, и детекция сводится к его чтению. На self-hosted с нестандартной установкой достаточно DOCKER_HOST в env-блоке джобы. Отдельная переменная TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE задаёт путь сокета, который монтируется внутрь служебных контейнеров — уборщика и compose-стека: если демон живёт не там, где привык, укажите её, иначе уборщик запустится, но смотреть будет не в тот сокет. Практический вывод: паника о необнаруженном хосте в CI почти всегда означает, что джоба попала не на тот раннер — например, на образ без Docker вовсе, — а не то, что библиотека сломалась.

Минимальный workflow

GO
# .github/workflows/ci.yml
name: tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: go test
        run: go test ./... -race -timeout 15m

Минимальный каркас: hosted-раннер с Docker из коробки, checkout, setup-go — он же кэширует модули Go между прогонами, — и один шаг go test. Флаг -race ловит гонки, -timeout задаёт общий бюджет пакета и убивает зависший прогон раньше лимита джобы: вы получаете красный тест со стек-трейсом, а не серую джобу без вывода. Никаких services и compose: контейнеры testcontainers поднимет сам.

Переменные окружения: RYUK и TESTCONTAINERS в CI#

Все настройки библиотеки читаются из ~/.testcontainers.properties и перекрываются переменными окружения — в CI это главный канал: env-блок джобы настраивает прогон без правок кода. Базовый набор такой. TESTCONTAINERS_RYUK_DISABLED выключает уборщика; по умолчанию он включён, и выключать его в CI — риск, о котором ниже. RYUK_CONNECTION_TIMEOUT (дефолт — одна минута) и RYUK_RECONNECTION_TIMEOUT (дефолт — десять секунд) управляют связью с уборщиком; устаревшие имена TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT и TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT ещё читаются, но новые предпочтительнее. TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED запускает контейнер уборщика в привилегированном режиме — понадобится там, где этого требует среда. TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX подставляет префикс к образам из Docker Hub — штатный путь для закрытых сетей с внутренним зеркалом. DOCKER_HOST указывает нестандартный демон, TESTCONTAINERS_SESSION_ID закрепляет идентификатор сессии. Значения — строки: true, false и длительности вида 2m; YAML-булевы значения в env превратятся в строки сами.

Проблема в CI — решение#

Проблема в CIРешение
первый прогон медленный: образы тянутся из реестрашаг pre-pull до go test; тяжёлым наборам образов — actions/cache для каталога демона
джоба убита по лимиту: t.Cleanup не выполнилсяуборщик Ryuk зачищает по меткам сессии; не задавать TESTCONTAINERS_RYUK_DISABLED=true
закрытая сеть: Docker Hub недоступенTESTCONTAINERS_HUB_IMAGE_NAME_PREFIX на внутреннее зеркало
сервис не успевает стартовать на медленном раннерезапас в wait-стратегии: WithStartupTimeout вместо sleep
параллельные джобы на общем self-hosted раннере плодят уборщиковTESTCONTAINERS_SESSION_ID: прогоны с одним значением делят одного уборщика
модули Go собираются каждый прогон зановоsetup-go с кэшем модулей и сборки
мусор от прошлых прогонов на self-hostedпериодическая чистка docker по меткам org.testcontainers; уборку не отключать

Кэш Docker-образов: ускоряем прогон#

На hosted-раннере всё тянется из сети заново: образ базы, брокера, уборщика — и медленный первый прогон почти всегда про pull, а не про тесты. Слои кэша Docker — ресурс демона, и жить они начинают с шага прогрева: docker pull каждого нужного образа до go test кладёт их в локальный кэш, и testcontainers при старте находит образ на месте, без похода в реестр. Это самый дешёвый в обслуживании вариант и почти всегда достаточный. Вариант тяжелее — actions/cache для каталога демона /var/lib/docker: он сохраняет и восстанавливает весь каталог разом, но требует прав root и остановки демона на время восстановления — усложнение оправдано, только если образы по-настоящему гигантские. Для образов, собираемых из Dockerfile, отдельная история — кэш слоёв сборки у билдера, чтобы go build внутри образа не стартовал с нуля; механика сборки разобрана в пятнадцатом уроке. Образы сервисов меняются редко, поэтому прогрев окупается на каждом следующем прогоне. Единственная оговорка: pre-pull имеет смысл для образов с фиксированными тегами — прогрев плавающего latest экономит меньше, потому что однажды всё равно вытянет новый слой.

env-блок и прогрев образов

GO
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      RYUK_CONNECTION_TIMEOUT: 2m
      RYUK_RECONNECTION_TIMEOUT: 30s
    steps:
      - uses: actions/checkout@v4

      - name: прогрев кэша образов
        run: |
          docker pull postgres:alpine
          docker pull redis:alpine

      - name: go test
        run: go test ./... -race -timeout 15m

env-блок джобы — CI-настройка библиотеки: таймауты связи с уборщиком взяты с запасом под медленный раннер. Шаг прогрева тянет образы сервисов до тестов: pull ложится в кэш демона, и testcontainers стартует контейнеры без похода в реестр. Значения env — строки; длительности вида 2m библиотека разбирает сама.

Флейки и таймауты: три слоя бюджета#

Флейк в CI почти всегда означает, что какой-то таймаут истёк раньше, чем должен был. Таймаутов три слоя, и они обязаны соотноситься: внутренний — стартовый таймаут wait-стратегии из урока об ожиданиях, средний — go test -timeout, внешний — лимит времени джобы. Правило простое: внутренние истекают раньше внешних, иначе вместо красного теста со стек-трейсом вы получаете убитую джобу без диагностики. Второй источник флейков — теснота: пакеты go test стартуют параллельно на все ядра, и слабый раннер одновременно тянет несколько контейнеров, гонки -race и сборку — память и порты кончаются раньше тестов. Ограничьте параллелизм пакетов флагом -p, а тестовую параллельность внутри пакета настраивайте осознанно. Запас в wait-стратегии решает первую половину проблемы: WithStartupTimeout вместо sleep даёт сервису дорасти до готовности без искусственных задержек в удачном прогоне. Третий источник — кэш: setup-go сохраняет и GOCACHE, где лежат результаты тестов, поэтому интеграционный прогон честнее с -count=1. Повторы упавшего теста лечат симптом: флейк — сигнал пересмотреть таймауты, а не добавить retry.

Проверка: уборка после убитого прогона

go test превысил -timeout и убил процесс: t.Cleanup не выполнился. Джоба на hosted-раннере, TESTCONTAINERS_RYUK_DISABLED не задан. Что станет с контейнерами?

Сессия и уборщик: кто приберёт за убитой джобой#

Как уборщик устроен в CI: вместе с первым контейнером прогона поднимается контейнер-уборщик — он монтирует docker-сокет, слушает управляющий порт (по умолчанию 8080, переназначается переменной RYUK_PORT) и работает с ресурсами по сессионным меткам семейства org.testcontainers. Процесс-владелец умер — уборщик зачищает всё, что тот насоздавал, от контейнеров до сетей и томов. Строки reaper в логах теста — норма, а не ошибка: это уборщик отчитывается о подключении. Ошибка reaper not found — уже сигнал: уборщик не поднялся — образ недоступен в закрытой сети без зеркала либо запрещён привилегированный режим; чинить надо среду, а не глушить уборку. На общем self-hosted раннере у каждой джобы свой уборщик со своей сессией; если параллельные прогоны плодят их слишком охотно, TESTCONTAINERS_SESSION_ID закрепляет идентификатор: прогоны с одним значением делят одного уборщика. Охотиться за проблемами подключения помогает RYUK_VERBOSE: подробный лог уборщика попадает в вывод теста, и причина — медленный сокет, запрет privileged, недоступный образ — видна сразу, а не угадывается по красной джобе.

Соберите путь джобы

Расставьте шаги: от старта джобы до страховки уборщика.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Соберём маршрут целиком: джоба на ubuntu-latest уже с Docker, setup-go кэширует модули и сборку, pre-pull греет образы, go test ./... -race -timeout держит бюджет, env-переменные настраивают уборщика под медленный раннер, а метки и сессия гарантируют уборку даже после убитого процесса. Это тот же жизненный цикл, что и локально, — меняется только среда, поэтому настройки живут в workflow, а не в коде. Дальше маршрут ветвится: поведение уборщика и его отключение — тема урока про Ryuk, wait-стратегии разобраны в двенадцатом уроке, а типовые грабли портов, скорости и флейков собраны в отдельном уроке про грабли. Остальной путь по курсу testcontainers — от модулей баз до итогового стенда. И держите пайплайн скучным: скучный зелёный workflow — признак системы, а не отсутствия жизни. Когда появится желание ускорить прогон радикально, вспомните режим Reuse из шестнадцатого урока: он одинаково работает и на ноутбуке, и в джобе.

Проверка: образы из зеркала

Сеть CI закрыта: Docker Hub недоступен, образы лежат во внутреннем зеркале. Какая переменная перенаправит pull'ы библиотеки?

Что останется после убитой джобы

Прочитайте код и выберите, что он выведет:

// Self-hosted runner: демон Docker общий для всех джоб.
// В workflow задано:
//   env:
//     TESTCONTAINERS_RYUK_DISABLED: "true"
// Джоба превысила лимит времени: процесс go test убит,
// t.Cleanup не успел выполниться.

Соберите каркас workflow

Восстановите минимальный workflow: раннер с Docker, ключ шага и гонки.

# .github/workflows/ci.yml name: tests on: push jobs: test: runs-on: steps: - uses: actions/checkout@v4 - name: go test : go test ./... - -timeout 15m

Типовые ошибки CI#

  • TESTCONTAINERS_RYUK_DISABLED=true «для скорости»: на self-hosted мусор остаётся навсегда
  • внутренний таймаут больше go test -timeout: красная джоба без диагностики вместо красного теста
  • pull без прогрева: каждый прогон заново тянет образы из реестра
  • actions/cache для /var/lib/docker без root и остановки демона: кэш пустой, шаг падает
  • -race и параллельные пакеты без запаса в таймаутах: раннер захлёбывается
  • sleep вместо wait-стратегии: на медленном раннере это гарантированный флейк
  • устаревшие имена TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT: ждёте эффекта от переменной, которая больше не читается
  • падение без сохранённых логов контейнеров: разбор не с чего начать

Карточки: testcontainers в CI

Термин

ubuntu-latest

нажмите, чтобы перевернуть
Определение

Хостед-раннер GitHub Actions с предустановленным Docker: демон запущен, CLI и сокет на месте. Джоба стартует с чистой машиной, поэтому уборка прошлогодних зомби не нужна, а pull всегда из сети.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • ubuntu-latest уже с Docker: демон, CLI и сокет на месте — testcontainers работает в CI без настройки инфраструктуры.
  • Конфиг читается из ~/.testcontainers.properties и перекрывается env: RYUK_DISABLED, таймауты RYUK, HUB_IMAGE_NAME_PREFIX, DOCKER_HOST, SESSION_ID.
  • Уборщика в CI не глушить: он прибирает за тестами, убитыми по -timeout, когда t.Cleanup не успел выполниться.
  • Прогрев образов pre-pull шагом дешевле кэша каталога демона; таймауты соотносимы: wait-стратегия внутри go test -timeout внутри лимита джобы.

Домашнее задание

Соберите минимальный workflow: checkout, setup-go, шаг прогрева двумя docker pull и шаг go test ./... -race -timeout 15m с env-блоком из RYUK_CONNECTION_TIMEOUT и RYUK_RECONNECTION_TIMEOUT. Локально повторите прогон с TESTCONTAINERS_RYUK_DISABLED=true и без неё, сравнив docker ps после каждого. Затем добавьте TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX на выдуманное зеркало и найдите в логах, как поменялись имена образов при pull'е.

Личные заметкиПусто
Хранятся только в вашем браузере (Testcontainers · CI: тесты в GitHub Actions)

Вопросы и ответы по уроку

Нужен ли в workflow блок services с базой, как в старых гайдах?

Нет: сервисы дублируют то, что testcontainers делает лучше. Библиотека сама создаёт контейнер, честно ждёт готовности wait-стратегией и убирает за собой, а services поднимают контейнер до тестов, без гарантий готовности и с ручной чисткой. Одна джоба с go test проще в поддержке: вся инфраструктура описана в коде тестов, а не в YAML рядом, и локально прогон воспроизводится тем же кодом.

Где настраивать библиотеку — в properties-файле или в env?

Файл ~/.testcontainers.properties удобен для личных настроек на машине разработчика, но в CI его некому создать, и он не версионируется. Переменные окружения в env-блоке джобы живут рядом с workflow, видны в диффе и применяются к каждому прогону одинаково — для CI это надёжный канал. Формат значений тот же: true, false и длительности вида 2m; env-значения перекрывают файл, поэтому забытое свойство в файле не переиграет CI.

Почему интеграционные тесты на CI идут дольше, чем локально?

Раннер слабее ноутбука по ядрам и диску, образы тянутся из сети, -race замедляет каждый тест, а параллельные пакеты множат контейнеры. Добавьте холодные кэши: первый прогон всегда самый медленный. Планируйте бюджет от CI, а не от ноутбука: таймауты с запасом, параллелизм пакетов под нагрузку раннера, прогрев образов и честные wait-стратегии вместо sleep.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Base25 уроков~12 чновичок25 уроков уже доступны

Go с нуля: базовый синтаксис и конструкции

Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.

  • Писать, компилировать и запускать программы на Go
  • Свободно работать с переменными, типами, операторами
  • Управлять потоком выполнения: if, switch, for, range
  • Хранить данные в массивах, срезах и картах