Плагины в Go-автотестах: как вынести инфраструктуру за пределы тестов
- пятница, 3 июля 2026 г. в 00:00:09
Кто хоть раз писал автотесты на Go, которые выходят за рамки простых unit-тестов, неизбежно сталкивался с одними и теми же вопросами.
Как красиво завернуть тест в отчёт, не превращая его в главный объект теста?
Как добавить сбор статистики?
Как посчитать количество упавших и прошедших тестов?
Как перезапускать только нестабильные тесты?
Как измерить время выполнения каждого теста?
Как запускать тесты по тегам или маркировкам?
Вопросов много — стандартных ответов нет.
И дело здесь даже не в Go как языке. Стандартный testing изначально спроектирован максимально простым и минималистичным. Он честно говорит: вот базовый интерфейс для тестов, а всё остальное — на ваше усмотрение.
На практике это «дальше сами» почти всегда заканчивается одинаково. Инфраструктурная логика начинает проникать прямо в тесты: отчётность, сбор статистики, фильтрация, повторные запуски, замеры времени. Код тестов обрастает обвязкой, дублируется из файла в файл и со временем превращается в хрупкий, трудноизменяемый фреймворк внутри проекта.
Любое изменение процесса — новый отчёт, другой формат статистики, иной способ фильтрации — требует правок по всему тестовому набору. Такие изменения становятся токсичными для проекта.
В этой статье разберёмся, как решить все эти задачи иначе: вынести инфраструктуру за пределы тестов и подключать нужное поведение одной строкой — через плагины в Axiom.
Когда в Go-проекте появляется потребность в отчётности, статистике или фильтрации тестов, всё почти всегда начинается одинаково: нужную инфраструктуру начинают добавлять прямо в тело тестов.
Например, если нужно подключить отчётность (условно — Allure), код теста начинает выглядеть примерно так.
func TestUserCreation(t *testing.T) { t.Parallel() allure.Test( t, allure.ID("123"), allure.Name("user can be created"), allure.Feature("users"), allure.Story("create user"), allure.Action(func() { client := NewClient() userID := client.CreateUser() resp := client.GetUser(userID) if resp.ID != userID { t.Fatalf("unexpected user id") } }), ) }
Формально тест всё ещё работает. Но обратите внимание, что произошло: инфраструктура отчётности стала частью тестового сценария. Чтобы добавить или изменить метаданные, формат отчёта или сам инструмент, теперь нужно править каждый тест.
Тест перестаёт быть сценарием проверки поведения и превращается в точку сборки отчётной инфраструктуры.
Со временем таких тестов становится десятки и сотни. Метаданные копируются, обёртки повторяются, код разрастается. Любое изменение — например, смена инструмента отчётности или структуры метаданных — требует массового рефакторинга всего проекта.
Когда появляется задача посчитать количество прошедших и упавших тестов, часто идут ещё дальше — вводят глобальные счётчики.
var passed int32 var failed int32 func TestSomething(t *testing.T) { if err := runScenario(); err != nil { atomic.AddInt32(&failed, 1) t.Fail() return } atomic.AddInt32(&passed, 1) }
Теперь тест знает:
что такое статистика,
как она считается,
и когда именно её нужно обновлять.
Это уже не тест. Это часть фреймворка, размазанная по тестам.
Следующий шаг — внешние скрипты, которые парсят артефакты тестов, потому что собрать статистику иначе уже невозможно.
PASSED=0 FAILED=0 for file in results/*.json; do status=$(jq -r '.status' "$file") if [ "$status" = "passed" ]; then PASSED=$((PASSED+1)) else FAILED=$((FAILED+1)) fi done
На этом этапе тесты, отчёты и инфраструктура окончательно перестают быть единым целым. Контракты неявные, зависимости хрупкие, а любое изменение процесса превращается в цепочку правок: тесты → хелперы → скрипты → CI.
Во всех этих примерах проблема одна и та же: инфраструктура живёт внутри тестов.
Тесты знают слишком много:
как формируется отчёт,
как считается статистика,
какие теги и фильтры используются,
какие инструменты подключены.
Любое изменение — новый формат отчёта, другая система статистики, новые правила запуска — требует изменений по всему проекту.
В какой-то момент такой тестовый код перестаёт быть поддерживаемым. Он живёт по простому правилу: работает — не трогай, сломалось — не починишь.
Проблема здесь не в конкретных инструментах, а в самом месте, где живёт инфраструктура. Именно эту проблему и призвана решать плагинная модель.
И тут на сцену выходит Axiom.
Axiom изначально спроектирован с простой, но жёсткой идеей: тесты должны оставаться тестами. Инфраструктура — отчётность, статистика, фильтрация, логирование, метрики — не должна жить внутри тестового сценария.
Более того, тест вообще не должен знать, какой именно инструмент используется под капотом. Тест оперирует только терминами тестирования: названием, приоритетом, тегами, feature, story, шагами выполнения. Всё остальное — ответственность рантайма.
Для этого в Axiom уже есть набор встроенных плагинов — они полностью опциональны: можно использовать готовые решения из коробки или писать свои, не меняя тесты.
Плагин в Axiom — это не вспомогательная функция и не обёртка вокруг теста. Это функция, которая подключается к execution model и регистрирует поведение в runtime:
оборачивает выполнение теста или шагов,
управляет skip-логикой,
добавляет контекст или метаданные.
Важно: плагин не исполняет тест и не меняет его код. Он лишь наблюдает и декорирует выполнение.
Чтобы увидеть, как это работает на практике, напишем минимальный плагин. Пусть он решает простую инфраструктурную задачу — измеряет время выполнения теста.
Важно: цель примера не в измерении времени, а в том, на каком уровне это делается.
package timed import ( "fmt" "time" "github.com/Nikita-Filonov/axiom" ) // Plugin возвращает axiom.Plugin — функцию, // которая получает runtime Config теста. func Plugin() axiom.Plugin { return func(cfg *axiom.Config) { // Регистрируем TestWrap — middleware вокруг выполнения теста. // Wrap применяется на уровне execution model, // а не внутри тестового кода. cfg.Runtime.EmitTestWrap(func(next axiom.TestAction) axiom.TestAction { return func(c *axiom.Config) { start := time.Now() // Выполняем сам тест next(c) // Этот код выполнится после завершения теста // (включая шаги, хуки и retry-попытки). fmt.Printf( "test %q finished in %s\n", c.Name, time.Since(start), ) } }) } }
Здесь важно несколько вещей:
Плагин работает с Config — runtime-снимком теста, который создаётся Runner’ом для конкретного выполнения. Он не знает ничего о теле теста, не имеет доступа к его логике и не вмешивается в сценарий.
Через Runtime.EmitTestWrap плагин регистрирует поведение, которое будет применено ко всем тестам, выполняемым через этот Runner. Это не обёртка функции и не helper — это часть execution model.
Теперь подключим этот плагин на уровне Runner’а.
var runner = axiom.NewRunner( axiom.WithRunnerPlugins( timed.Plugin(), ), )
На этом инфраструктурная часть закончена.
func TestUserCreation(t *testing.T) { c := axiom.NewCase( axiom.WithCaseName("user can be created"), axiom.WithCaseMeta( axiom.WithMetaFeature("users"), axiom.WithMetaStory("create user"), axiom.WithMetaTag("smoke"), ), ) runner.RunCase(t, c, func(cfg *axiom.Config) { cfg.Step("create user", func() { // бизнес-логика }) cfg.Step("validate response", func() { // проверки поведения }) }) }
Тест по-прежнему описывает только сценарий и проверки. В нём нет:
логики измерения времени;
форматирования вывода;
привязки к конкретному инструменту;
инфраструктурных зависимостей.
Если плагин убрать или заменить — тест останется неизменным.
Именно так решается главная проблема Go-автотестов: любое инфраструктурное изменение перестаёт требовать переписывания тестов.
Инфраструктурные решения живут на уровне Runner’а и плагинов. Они подключаются декларативно, композиционно и централизованно. Тесты при этом остаются стабильными и читаемыми.
Смена отчётности, добавление статистики, фильтрации по тегам, логирования или метрик больше не требует переписывания тестов. Меняется только конфигурация execution runtime.
Именно это отличает плагинную модель от хелперов и обёрток — и именно поэтому Axiom масштабируется там, где самодельные решения начинают ломаться.
Автотест — это описание проверяемого поведения. Он не должен знать, как формируется отчёт, куда отправляются логи, по каким правилам считается статистика и как именно запускаются тесты в CI.
Все эти решения — инфраструктурные. Они живут в другом слое и меняются по другим причинам.
Плагинная модель Axiom разделяет эти миры. Тесты остаются декларативными и стабильными, а отчётность, фильтрация, логирование, retry и сбор метрик подключаются и изменяются отдельно — на уровне execution runtime.
Именно так тесты перестают быть хрупким набором костылей и превращаются в систему, которую можно развивать без страха сломать всё остальное.