Плагины в Go-автотестах: как вынести инфраструктуру за пределы тестов
Вступление
Кто хоть раз писал автотесты на 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 — это не хелперы
Плагин в 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.
Именно так тесты перестают быть хрупким набором костылей и превращаются в систему, которую можно развивать без страха сломать всё остальное.