惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Vercel News
Vercel News
Microsoft Azure Blog
Microsoft Azure Blog
Stack Overflow Blog
Stack Overflow Blog
Martin Fowler
Martin Fowler
Hacker News - Newest:
Hacker News - Newest: "LLM"
Cyberwarzone
Cyberwarzone
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Threat Research - Cisco Blogs
Know Your Adversary
Know Your Adversary
Recent Announcements
Recent Announcements
L
LINUX DO - 热门话题
D
DataBreaches.Net
K
Kaspersky official blog
T
Threatpost
F
Full Disclosure
T
The Exploit Database - CXSecurity.com
C
CERT Recently Published Vulnerability Notes
S
Securelist
I
Intezer
有赞技术团队
有赞技术团队
罗磊的独立博客
爱范儿
爱范儿
S
Schneier on Security
P
Privacy & Cybersecurity Law Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
LangChain Blog
美团技术团队
G
Google Developers Blog
T
Tor Project blog
Project Zero
Project Zero
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Hacker News
The Hacker News
W
WeLiveSecurity
Engineering at Meta
Engineering at Meta
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
阮一峰的网络日志
阮一峰的网络日志
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
P
Proofpoint News Feed
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Автоматизация тестирования на Go: стратегия и реализация с нуля
dim_dimyych_ · 2026-05-05 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение11 мин

Охват и читатели52

Обзор

Всем привет! Меня зовут Дима, я QA-инженер по автоматизации в Туту.

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

Уже больше полутора лет я пишу автотесты на Go. За это время мы прошли путь от «зачем вообще тестировать на Go?» до «почему мы не сделали это раньше?».

В этой статье я покажу, как внедрить автоматизацию тестирования на Go с нуля — так, чтобы она решала реальные бизнес‑проблемы, а не просто увеличивала количество тестов в репозитории.

Что разберем на практике:

  • почему автотесты на Go — это не просто альтернативный стек, а способ сделать тесты быстрее, стабильнее и ближе к коду;

  • как изолировать внешние зависимости через моки и не ломать тесты при каждом изменении API;

  • как корректно тестировать Kafka и БД, чтобы ловить реальные интеграционные ошибки;

  • как встроить всё это в CI/CD и получить быстрый, предсказуемый фидбек перед релизом.

Речь пойдёт о реальных микросервисных сценариях: gRPC, HTTP, Postgres, Kafka.

Почему выбрали Go для автотестов

Когда я только начинал, главный вопрос звучал так: зачем писать автотесты на Go, если есть Java и Python с десятками фреймворков.

Аргумент «чтобы разработчикам было проще читать код» звучит слабо. Поэтому решение должно было опираться не на удобство, а на экономику и устойчивость разработки.

Меньше кода — меньше расходов на поддержку. Если бэкенд написан на Go, тесты на Go позволяют использовать те же самые модели, protobuf-контракты и gRPC-клиенты.

Это означает, что нам не нужно поддерживать второй слой инфраструктуры на другом языке: со своими DTO, сериализаторами, клиентами и конфигурациями — обновление контрактов происходит централизованно через Go-модули без необходимости поддерживать отдельную реализацию клиентов на другом языке.

В проектах на смешанных стеках тестовый слой постепенно начинает жить собственной жизнью. Любое изменение контракта тянет за собой правки в двух местах. Это дополнительные часы работы, больше точек отказа и выше вероятность расхождения между тестами и реальностью.

Скорость сборки = скорость обратной связи. Go компилируется быстро и не требует виртуальной машины. На практике это означает короткий цикл «написал тест → запустил → получил результат». Чем быстрее команда получает обратную связь, тем быстрее исправляются дефекты.

А скорость обратной связи напрямую влияет на time-to-market. Быстрее релизы → быстрее вывод фич → быстрее получение выручки или пользовательской ценности.

Простота языка снижает риски. Go намеренно ограничивает сложность. В нём трудно перемудрить с абстракциями или построить чрезмерно запутанную архитектуру.

Для тестового кода это особенно важно. Тесты должны быть предсказуемыми, понятными и легко поддерживаемыми даже спустя год.

Чем проще код тестов, тем ниже зависимость от конкретных людей в команде. Это уже управленческий риск: если ключевой инженер уходит, бизнес не должен терять способность развивать продукт.

Архитектура, которую будем тестировать

  • fly-broker — gRPC-сервис бронирования

  • avia-service HTTP-сервис, выпускающий билеты

  • PostgreSQL

  • Kafka (через выгрузку из Outbox)

Схема взаимодействия выглядит так:

Эта схема показательна тем, что сочетает все ключевые источники сложности в тестировании микросервисов: синхронные вызовы между сервисами, работу с БД и асинхронную доставку событий через Kafka. 

На таком примере можно разобрать и компонентные, и полноценные интеграционные тесты.

Пишем тесты с Allure-Go

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

Установим зависимости:

go get github.com/ozontech/allure-go/pkg/allure
go get github.com/ozontech/allure-go/pkg/framework

Базовая структура теста выглядит так:

type CreateOrderSuite struct {
	setup.TestSuite
	ParamCreateOrderPositiveTestData []CreateOrderPositiveTestData
	ParamCreateOrderNegativeTestData []CreateOrderNegativeTestData
}

func (s *CreateOrderSuite) BeforeAll(t provider.T) {
	s.Setup(t)
	s.ParamCreateOrderPositiveTestData = GetCreateOrderPositiveTestData
	s.ParamCreateOrderNegativeTestData = GetCreateOrderNegativeTestData
}

// SuiteRunner запускает все тесты из сьюта CreateOrderSuite
func TestCreateOrderSuiteRunner(t *testing.T) {
	suite.RunNamedSuite(t, constants.CreateOrderSuite, new(CreateOrderSuite))
}


// Параметризованные тесты
func (s *CreateOrderSuite) TableTestCreateOrderPositiveTestData(t provider.T, data CreateOrderPositiveTestData) {
	t.Parallel()
	t.Title(data.testName)
	t.Description("Создаем заказ. После сверяем статус заказа с ожидаемым результатом")
	t.AddSubSuite(constants.PositiveSubSuite)

	t.WithNewStep("Создаем заказ", func(sCtx provider.StepCtx) {
		ctx := t.Context()
		ctx = metadata.AppendToOutgoingContext(ctx, constants.AccountIDHeader, data.accountID)

		resp, err := s.FlyBrokerClient.CreateOrder(ctx, data.request)
		s.AssertGrpcResponseSuccess(sCtx, resp, err)
		sCtx.Require().Equal(data.bookingStatus.String(), resp.GetBookingStatus().String(), "Проверяем, что статус заказа соответствует ожидаемому результату")
	})
}

Про параметризованные тесты

Для корректной работы параметризованных тестов и их правильного отображения в Allure необходимо придерживаться единых правил именования и инициализации.

Функция с тестами

Функция, выполняющая параметризованные тесты:

  • Должна начинаться с префикса TableTest

  • Должна заканчиваться названием структуры с тестовыми данными

  • Вторым аргументом должна принимать структуру с тестовыми данными

Пример:

func TableTestCreateOrderPositiveTestData(t provider.T, data CreateOrderPositiveTestData)

Структура сьюта

Структура сьюта для параметризованных тестов:

  • Должна начинаться с префикса Param 

  • Должна заканчиваться названием структуры с тестовыми данными

Пример: ParamCreateOrderPositiveTestData

Инициализация тестовых данных

Структуре сьюта необходимо присвоить список тестовых данных

Пример:

s.ParamCreateOrderPositiveTestData = GetCreateOrderPositiveTestData

Переменная GetCreateOrderPositiveTestData должна возвращать срез тестовых данных

var GetCreateOrderPositiveTestData = []CreateOrderPositiveTestData{ //Данные теста }


Что даёт provider.T

  • t.Parallel() // параллельный запуск тестов

  • t.Skip() // Скип теста

  • t.Assert() / t.Require() // Софт / хард - ассерты

  • t.WithTestSetup() / t.WithTestTeardown() // сетап / завершение теста

  • t.Title(), t.Description(), t.Epic() // более подробное описание тестов

Важно понимать, что provider.T — это не просто прокси-обертка над *testing.T, а фреймворк, который управляет тестом и формированием Аllure-отчета.

Написание моков

На тестовой среде avia-service работает медленно и периодически падает.

Каждый такой сбой:

  • ломает прогон автотестов,

  • заставляет команду перезапускать пайплайны,

  • тратит часы на разбор «это баг или сбоит среда?»,

  • замедляет релизы.

Если тестовый прогон занимает 30–40 минут и падает из‑за нестабильного внешнего сервиса хотя бы 2–3 раза в день, команда теряет несколько человеко‑часов ежедневно. В пересчёте на месяц это уже десятки часов разработки, потраченных не на фичи, а на борьбу с инфраструктурой.

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

Нужно:

  • тестировать позитивные сценарии

  • проверять таймауты

  • проверять ошибки провайдера

Решение — собственный mock-service, позволяющий управлять сценариями.

Для написания mock-service будем использовать библиотеку go-restful.

Создаем хэндлер:

type AviaHandler struct{}

func NewAviaHandler() *AviaHandler {
	return &AviaHandler{}
}


func (h *AviaHandler) IssueTicket(req *restful.Request, resp *restful.Response) {
	if req.Request.Body == http.NoBody {
		_ = resp.WriteError(http.StatusBadRequest, errors.New("request body is required"))
		return
	}

	defer func() {
		_ = req.Request.Body.Close()
	}()

	bb, err := io.ReadAll(req.Request.Body)
	if err != nil {
		_ = resp.WriteError(http.StatusBadRequest, fmt.Errorf("read body err: %w", err))
		return
	}

	var requestBody model.IssueTicketRequest
	if err = json.Unmarshal(bb, &requestBody); err != nil {
		_ = resp.WriteError(http.StatusBadRequest, fmt.Errorf("parse body err: %w", err))
		return
	}

	var data model.IssueTicketResponse
	orderID := requestBody.OrderID

	switch orderID {
	case "success":
		data = model.IssueTicketResponse{
			// заполнение успешного ответа
		}
	case "timeout":
		time.Sleep(5 * time.Second)
	case "error":
		_ = resp.WriteError(
			http.StatusInternalServerError,
			fmt.Errorf("provider error"),
		)
		return
	}

	if err := resp.WriteEntity(data); err != nil {
		_ = resp.WriteError(http.StatusInternalServerError, err)
	}
}

Делаем роут:

func InitAviaRoutes() *restful.WebService {
	webService := &restful.WebService{}
	aviaHandler := avia.NewAviaHandler()

	webService.
		Path("/avia").
		Produces(restful.MIME_JSON).
Route(webService.POST("/tickets/issue").To(aviaHandler.IssueTicket))

	return webService
}

Теперь мы можем контролировать поведение провайдера через order_id.

Это даёт команде не просто набор сценариев, а управляемую тестовую среду:

  • Тестировать happy path — проверять основной бизнес‑сценарий без влияния внешней нестабильности.

  • Проверять ошибки от провайдера — 4xx/5xx, некорректные ответы.

  • Эмулировать таймауты и проверять обработку деградаций.

В результате команда получает:

  • предсказуемые и воспроизводимые тесты;

  • отсутствие флаки‑падений из‑за внешней среды;

  • быстрое прохождение тестов в CI;

  • возможность покрыть редкие и аварийные сценарии, которые сложно воспроизвести на реальном стенде.

Тестирование Kafka и БД

Покрывая связку БД + Kafka, мы проверяем:

  • что события действительно публикуются;

  • что они публикуются корректно;

  • что интеграция между слоями не ломается при изменениях;

  • что бизнес‑процесс гарантированно доходит до конца;

Сервис fly-broker пишет события в таблицу outbox, а отдельный воркер отправляет их в Kafka.

Установим зависимости:

gorm.io/driver/postgres для взаимодействия с БД

github.com/segmentio/kafka-go для тестирования кафки

Подключаемся к БД Postgres через gorm:

db, err := gorm.Open(postgres.Open(dbDSN), &gorm.Config{})

dbDSN — переменная, по которой задается конфигурация БД.

Настройка БД завершена. Можно с ней взаимодействовать.

func (r *Repository) AddOutbox(ctx context.Context, outbox model.Outbox) error {
	if err := r.db.WithContext(ctx).Create(&outbox).Error; err != nil {
		return fmt.Errorf("failed to add outbox record: %w", err)
	}

	return nil
} 

func (r *Repository) DeleteOutbox(ctx context.Context, id string) error {
	if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Outbox{}).Error; err != nil {
		return fmt.Errorf("failed to delete outbox record: %w", err)
	}

	return nil
}

Kafka-consumer для теста:

type Consumer struct {
	reader *kafka.Reader
}

func NewConsumer(topic, consumerGroup string, brokers ...string) *Consumer {
	return &Consumer{
		reader: kafka.NewReader(kafka.ReaderConfig{
			Brokers:        brokers,
			Topic:          topic,
			GroupID:        consumerGroup,
			CommitInterval: 0,
		}),
	}
}

func (c *Consumer) Close() error {
	return c.reader.Close()
}

func (c *Consumer) ReadMessage(ctx context.Context) (kafka.Message, error) {
	msg, err := c.reader.ReadMessage(ctx)
	if err != nil {
		return kafka.Message{}, fmt.Errorf("failed to read message: %w", err)
	}
	if err := c.reader.CommitMessages(ctx, msg); err != nil {
		return kafka.Message{}, fmt.Errorf("failed to commit message: %w", err)
	}

	return msg, nil
}

Важно

  • CommitInterval: 0 — отключение автокоммита, где сообщение считается успешно обработанным только после успешного коммита reader.CommitMessages(ctx, msg)

  • reader.Close(): завершаем работу с топиком, чтобы избежать утечек

Тест на кафку:

// Уже единичный тест, без параметризации 
func (s *KafkaSuite) TestKafka(t provider.T) {
	t.Parallel()
	t.Title("Проверяем отправку сообщений в кафку из таблицы outbox")
	t.Description("Добавляем запись в таблицу outbox и проверяем, что запись по тикеру отправится в кафку")
	t.AddSubSuite(constants.PositiveSubSuite)

	outbox := createNewOutbox(model.StateProcessing, uuid.New().String(), faker.Sentence(), 10000)

	t.WithTestSetup(func(setupProvider provider.T) {
		err := s.Repo.AddOutbox(setupProvider.Context(), outbox)
		setupProvider.Require().NoError(err, "Проверяем, что запись в таблицу outbox успешно добавлена")
	})
	
	// Этот блок гарантированно выполнится, даже если дальнейшие шаги завершатся ошибкой
	defer func() {
		t.WithTestTeardown(func(teardownProvider provider.T) {
			err := s.Repo.DeleteOutbox(t.Context(), outbox.ID)
			teardownProvider.Require().NoError(err, "Проверяем, что запись из таблицы outbox успешно удалена")
		})
	}()

	t.WithNewStep("Проверяем отправку outbox в кафку", func(sCtx provider.StepCtx) {
		found, kafkaMessage := s.GetMessageFromOutboxTopic(t, outbox.ID, 15*time.Second)
		sCtx.Assert().True(found, fmt.Sprintf("Проверяем, что сообщение из топика outbox по id %v найдено", outbox.ID))
	})
}

Запуск тестов

Тесты запускаем через команду:

rm -rf tests/allure-results &&
go clean -testcache && godotenv -f .env gotestsum --format testname – -p 1 ./…

Из особенностей этой команды:

  • -rf tests/allure-results— перед запуском тестов удаляется папка allure-results, чтобы очистить старые результаты и сформировать отчет заново

  • go clean -testcache — в Go кэшируются успешные тесты. И если код не менялся, то такие тесты заново не запускаются. Эта команда очищает кэш тестов, чтобы они гарантированно выполнялись 

  • -format testname — простой формат по именам тестов. В консоли будут отображаться названия тестов

  • - p 1 — ограничение по параллельности: запускаем 1 пакет за раз. Бывает полезна, чтобы, например, избежать race condition

Чтобы указать путь allure-results, нужно задать переменную окружения ALLURE_OUTPUT_FOLDER для которой указываем абсолютный путь.

После прохождения тестов сформируем аллюр-отчет:

allure generate tests/allure-results --clean -o 
tests/allure-report
	allure open tests/allure-report

Интеграция в CI/CD

В качестве CI/CD системы будем использовать Gitlab CI. Тесты запускаются в Docker-окружении, что позволяет максимально приблизить выполнение к реальной среде.

Для запуска тестов нам потребуется полноценная тестовая инфраструктура. В пайплайне поднимаются следующие контейнеры.

fly-broker — контейнер состоит из:

  • PostgreSQL

  • Kafka

  • запущенное приложение fly-broker

mock-service — контейнер включает:

  • запущенный http-сервис, который эмулирует поведение внешнего сервиса avia-service

fly-broker-tests:

  • контейнер, в котором выполняется запуск тестов

Сетевая конфигурация

Все контейнеры объединяются в единую docker-сеть, что позволяет полностью изолировать тестовое окружение и обращаться к тестам по имени контейнера.

Такой подход делает запуск тестов:

  • независимым от тестовой инфраструктуры

  • пригодным для локального запуска через docker-compose

Подробная реализация описана в Dockerfile и docker-compose.yml (ссылки приводятся в репозитории).

Настройка .gitlab.ci.yml

Интеграционные тесты выполняются внутри джобы integration-tests.

Что происходит в самой job:

  1. Создается единая сеть fly-broker-network, к которой будут подключаться контейнеры

  2. Поднимаются контейнеры fly-broker, mock-service, fly-broker-tests

  3. Выполняется проверка healthcheck: проверяем подключение к БД, доступность кафки, готовность приложения

  4. Сохранение allure-артефактов с тестами

  5. Джоба допускает падение

before_script:
    - docker network create fly-broker-network || true
  script:
    - docker-compose up -d --build
    - |
      echo "Waiting for fly-broker app (migrations + gRPC) to be ready..."
      for i in $(seq 1 60); do
        status=$(docker inspect fly-broker-app --format '{{.State.Health.Status}}' 2>/dev/null || echo "starting")
        [ "$status" = "healthy" ] && echo "Broker is ready." && break
        [ $i -eq 60 ] && echo "Broker did not become healthy in time." && exit 1
        sleep 2
      done
    - echo "Starting integration tests (fly-broker-tests)..."
    - cd fly-broker-tests && docker-compose build --no-cache tests && docker-compose run --rm tests
  artifacts:
    when: always
    paths:
      - fly-broker-tests/tests/allure-results
after_script:
  allow_failure: true

Публикация Allure-отчета

Формирование Allure-отчета выполняется внутри джобы Allure-report:

allure-report:
  stage: allure
  needs: [integration-tests]
  script:
    - mkdir -p fly-broker-tests/tests/allure-report
    - allure generate fly-broker-tests/tests/allure-results --clean -o fly-broker-tests/tests/allure-report
    - echo "Allure-report link - https://${CI_PROJECT_NAMESPACE}.${CI_PAGES_DOMAIN}/-/${CI_PROJECT_NAME}/-/jobs/${CI_JOB_ID}/artifacts/fly-broker-tests/tests/allure-report/index.html"
  artifacts:
    when: always
    paths:
      - fly-broker-tests/tests/allure-report
  allow_failure: false

CI_PROJECT_NAMESPACE, CI_PAGES_DOMAIN, CI_PROJECT_NAME, CI_JOB_ID — стандартные предопределенные переменные Gitlab, про них можно почитать по этой ссылке

Пример ссылки, которая будет сформирована: https://dimyych_02-group.gitlab.io/-/fly-broker/-/jobs/13491558366/artifacts/fly-broker-tests/tests/allure-report/index.html

Заключение

В этой статье я рассказал про автоматизацию тестирования на Go и как это проектировать с нуля и интегрировать в CI/CD.

Переход на Go дал не только технические преимущества, но и организационный эффект: тесты перестали жить отдельно от разработки. Один язык, общие модели, понятный и быстрый CI сократили время поддержки и уменьшили риск архитектурного рассинхрона.

Ссылки

Ссылка на тестовый проект

Полезные ссылки по фреймворку Allure-go: 

Kafka-go

Go-Restful

Спасибо за внимание!