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

推荐订阅源

月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
Jina AI
Jina AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
雷峰网
雷峰网
T
Tailwind CSS Blog
MongoDB | Blog
MongoDB | Blog
博客园 - 【当耐特】
博客园 - 聂微东
V
Visual Studio Blog
博客园_首页
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
GbyAI
GbyAI
F
Fortinet All Blogs
C
Check Point Blog
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания
Retry в Go автотестах: как перестать бояться flaky-тестов
Никита Филонов · 2026-06-24 · via Все публикации подряд на Хабре

Средний

5 мин

253

Вступление

Flaky-тесты — неизбежная реальность, как бы нам этого ни хотелось. Особенно в интеграционных и E2E-сценариях, где автотесты зависят от сети, окружения и нестабильных тестовых стендов. Любой временный сбой — и тест падает, а его перезапуск превращается в проблему.

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

Сразу важно обозначить: flaky-тесты не стоит лечить ретраями в первую очередь. В идеале проблема должна решаться на уровне инфраструктуры или тестового сценария. Но если это невозможно и retry всё же необходим — его важно реализовать правильно.

В этой статье разберёмся, какие подходы к retry в Go приводят к проблемам и как выглядит корректная модель повторного запуска автотестов. В качестве примера будем использовать Axiom.

Как обычно реализуют retry в Go

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

Наивный retry

func TestSomething(t *testing.T) {
	for i := 0; i < 3; i++ {
		err := runTestScenario()
		if err == nil {
			return
		}
	}

	t.Fatal("test failed after retries")
}

На первый взгляд всё выглядит логично: если тест упал из-за временной проблемы, мы просто попробуем ещё раз. Но здесь есть фундаментальная проблема — повторяется только функция, а не сам тест.

В результате:

  • состояние между попытками не сбрасывается;

  • ресурсы переиспользуются повторно;

  • побочные эффекты накапливаются;

  • поведение теста становится недетерминированным.

Фактически это не retry теста, а несколько вызовов одной и той же логики в одном и том же контексте.

Или ещё хуже

Часто следующий шаг — попытка «сделать красиво» через t.Run:

func TestSomething(t *testing.T) {
	for i := 0; i < 3; i++ {
		t.Run(fmt.Sprintf("attempt-%d", i), func(t *testing.T) {
			runTestScenario(t)
		})
	}
}

Здесь кажется, что мы запускаем тест заново. Но на практике это всё тот же один тест с общим состоянием:

  • фикстуры и ресурсы не переинициализируются;

  • контекст и глобальное состояние протекают между попытками;

  • невозможно гарантировать чистый lifecycle;

  • CI начинает вести себя непредсказуемо.

Такой retry часто делает flaky-тесты ещё более flaky, чем они были изначально.

В обоих случаях проблема одна и та же: retry реализуется как повторный вызов кода, а не как повтор модели выполнения теста.

Как сделать retry правильно

После антипримеров важно зафиксировать одну ключевую мысль:

retry — это не повторный вызов кода теста.
retry — это повтор всей модели выполнения теста.

Именно здесь большинство самодельных решений в Go дают сбой. Они повторяют тестовую функцию, но не пересобирают окружение, не сбрасывают состояние и не изолируют попытки на уровне инфраструктуры.

В Axiom retry встроен в сам механизм выполнения теста. Он не оборачивает тестовую логику в цикл и не пытается «поймать» ошибку. Вместо этого каждая попытка запускается как отдельное выполнение теста с новым runtime-состоянием.

Важно сразу подчеркнуть: никакой магии здесь нет. Retry в Axiom — это всё тот же повторный запуск теста. Разница в том, на каком уровне он выполняется.

Минимальный пример retry в Axiom

package example_test

import (
	"fmt"
	"testing"
	"time"

	"github.com/Nikita-Filonov/axiom"
)

func TestRetryExample(t *testing.T) {

	// -------------------------------------------------------------------------
	// Runner-level retry policy
	// -------------------------------------------------------------------------
	// Глобальная политика retry для всех тестов,
	// если Case не переопределяет её явно.
	runner := axiom.NewRunner(
		axiom.WithRunnerRetry(
			axiom.WithRetryTimes(2),
			axiom.WithRetryDelay(1*time.Second),
		),
	)

	// -------------------------------------------------------------------------
	// Case-level override
	// -------------------------------------------------------------------------
	// Конкретный тест может переопределить retry-политику.
	c := axiom.NewCase(
		axiom.WithCaseName("retry example"),
		axiom.WithCaseRetry(
			axiom.WithRetryTimes(3),
			axiom.WithRetryDelay(500*time.Millisecond),
		),
	)

	attempt := 0

	runner.RunCase(t, c, func(cfg *axiom.Config) {

		attempt++
		fmt.Println("attempt:", attempt)

		// Имитируем flaky-поведение
		if attempt < 3 {
			t.Fail() // триггерит retry
		}

		cfg.Step("finalize", func() {
			fmt.Println("success on attempt", attempt)
		})
	})
}

Здесь retry задаётся декларативно и не вмешивается в код теста. Политика может быть определена глобально и при необходимости переопределена локально — без копипаста и без условной логики внутри тестов.

Что именно происходит при retry в Axiom

Когда тест запускается через Runner, Axiom сначала собирает полную конфигурацию выполнения — объединяя настройки Runner’а и конкретного Case. Эта конфигурация описывает всё, что относится к выполнению теста: метаданные, retry-политику, контекст, хуки, фикстуры, плагины и правила исполнения.

При retry происходит следующее:

  1. Конфигурация теста собирается заново. Для каждой попытки Axiom создаёт новый runtime-снимок (Config), объединяя Runner и Case. Это означает, что retry — это не повтор в рамках уже существующего состояния, а новый запуск с теми же правилами.

  2. Контекст и runtime не переиспользуются. Контекст выполнения, runtime-обёртки и execution-политики инициализируются заново. Ничего не «протекает» между попытками, если это явно не задано конфигурацией.

  3. Фикстуры получают новый жизненный цикл. Все фикстуры считаются ленивыми и привязанными к конкретной попытке. При retry они будут пересозданы при первом обращении и корректно очищены после завершения попытки.

  4. Хуки и плагины применяются повторно. Before/after-хуки, middleware и плагины выполняются так же, как при обычном запуске теста — но для каждой попытки отдельно.

И только после этого выполняется тело теста.

Таким образом, retry в Axiom — это повтор execution model, а не повтор функции.

Под капотом retry может выглядеть как обычный цикл — и это нормально. Ключевая разница не в реализации, а в точке повтора.

В Axiom повторяется не вызов тестовой функции, а вся модель выполнения теста: конфигурация, контекст, фикстуры и lifecycle. Благодаря этому retry остаётся предсказуемым, изолированным и управляемым даже в больших тестовых наборах.

Заключение

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

Axiom позволяет задать retry как часть тестового рантайма: политика повторных попыток конфигурируется централизованно на уровне Runner’а и при необходимости переопределяется локально на уровне конкретного теста. Это даёт контроль и предсказуемость — retry перестаёт быть хаотичным костылём и становится осознанной политикой выполнения.

Такой подход не лечит flaky-тесты, но позволяет работать с ними аккуратно: без протекания состояния, без скрытых побочных эффектов и без необходимости встраивать retry-логику прямо в тестовый код.