Обновить
2
Dmitry@Dimmur

AQA Java / Go

0,1
Рейтинг
Отправить сообщение

Спасибо за подробный деф, но, честно говоря, ваши аргументы только подтвердили то, о чем я писал) Вместо решения системных проблем вы пытаетесь выдать баги проектирования за фичи

Давайте разберем вашу защиту по пунктам, потому что дьявол кроется в исходниках :)

1. Пустые параметры: вы не решили проблему, а переложили ее на пользователя

Вы пишете:

если запуск с нуля параметрами является нежеланным поведением (это не всегда так), то достаточно лишь сделать на это проверку:

В этом-то и соль. Фреймворк стоит в точке (collector.go), где он точно знает, что конкретный параметризованный тест прогонится ровно 0 раз. Но вместо Fail, Skip или паники он просто тихонько выплевывает варнинг в stderr и возвращает nil.

if len(values) == 0 {
    fmt.Fprintf(os.Stderr, "testo: warning: ... will not run\n")
    return nil
}

Совет "ну допишите руками проверку в BeforeAll на каждый сьют" - это классический перевод стрелок. Зачем мне тогда вообще ваш фреймворк, если я должен сам руками страховать его логику?)))

Ваше сравнение с обычным table-driven тестом некорректно. В обычном Go-коде цикл у меня перед глазами, и я сам полностью контролирую семантику выполнения:

cases := fetchCases() 
if len(cases) == 0 { 
    t.Fatal("no cases") 
} 

for _, tt := range cases { ... }

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

2. Тип T — это не обычный Go-код, а рантайм-схема

Вы пишете, что структура T предназначена исключительно для подключения плагинов.

Но это как раз и доказывает мою мысль: T в Testo - это не обычный пользовательский тип. Это декларативная runtime-схема для вашего скрытого DI-контейнера. Фреймворк бесцеремонно лезет рефлексией в поля, требует только указатели, сам все аллоцирует и сам строит граф зависимостей. Шаг влево - рантайм-паника на старте:

if field.Kind() != reflect.Pointer {
    panic("testo: all exported fields in T must be pointers")
}

setPlugins(field.Addr(), plugins, specs)

Ссылка на go vet - это вообще мимо. go vet - это внешний статический линтер, а не система типов Go и не компиляторные гарантии. Его можно отключить, проигнорировать, криво настроить на CI или тупо не знать про его существование :) Он никак не превращает ваши рантайм-костыли в легитимный контракт языка. Если корректность структуры проверяется не компилятором, а вашим reflection-конструктором - это рантайм-модель, как ее ни называй.

3. Два одинаковых плагина - это не антипаттерн, а нормальная композиция

Вы пишете:

Я не думаю, что есть реальный сценарий, когда необходимо иметь две регистрации одного плагина.

Говорить так - значит расписаться в непонимании гибкой архитектуры и композиции. Такой сценарий элементарен: последовательная фильтрация тестов несколькими независимыми условиями. В том же Axiom можно спокойно сделать так:

axiom.WithRunnerPlugins(
    testtags.Plugin(testtags.WithConfigInclude(os.Getenv("REGION"))),
    testtags.Plugin(testtags.WithConfigInclude(os.Getenv("TEST_TAGS"))),
    testplugin.AllurePlugin(),
)

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

В Axiom плагины - это просто слайс функций type Plugin func(cfg *Config), они хранятся в массиве и предсказуемо применяются в том порядке, в котором их передал инженер:

for _, p := range c.Runner.Plugins { 
    p(c) 
}

В Testo это сделать невозможно физически, потому что у вас identity плагина - это reflect.Type:

plugins := make(map[reflect.Type]testoplugin.Plugin, len(pluginTypes))

Один тип - один инстанс. Все. Это не "безгранично гибкая система плагинов", это кастрированный Singleton-by-type Service Locator.

4. Потеря порядка плагинов из-за Go map

Вот тут начинается самое прекрасное))) Вы рассказываете в докладах про приоритеты и SortStableFunc, но ваш код теряет исходный порядок плагинов еще до того, как доходит до сортировки.

Вы собираете плагины в map[reflect.Type], потом спецификации складываете в map[reflect.Type]testoplugin.Spec, а потом передаете их дальше через итерацию по мапе. Как мы все знаем, порядок обхода map в Go рандомизируется на уровне рантайма языка.

Если у пользователя объявлено три плагина с дефолтным приоритетом 0, порядок выполнения их хуков превращается в рулетку. Порядок их работы - это не порядок их объявления инженером в структуре T, а следствие случайного обхода мапы. Чтобы это починить, пользователю придется руками расставлять приоритеты там, где все изначально должно было работать по цепочке.

В Axiom такой проблемы нет физически. Передал плагины в слайс - они ровно в этом порядке друг за другом и выполнились:

WithRunnerPlugins(p1, p2, p3)

// и получил применение:

p1(cfg) 
p2(cfg) 
p3(cfg)

Без рефлексии, без итерации по мапам и без костылей с ручной сортировкой приоритетов.

5. Сравнение с Axiom по reflection - это ложная эквивалентность

Ваша попытка ткнуть пальцем в Axiom:

Не считаю это проблемой, но справедливости ради, это же не так:

  1. Тут сканирование через рефлексию

  2. Тут паника из-за отсутствия указателей

это классический прием подмены понятий :)

Да, в Axiom есть рефлексия в методе RunSuite. Но RunSuite в Axiom - это опциональный адаптер-переходник для тех, кто привык к старым xUnit-сьютам. Дока тык. Основной движок Axiom полностью прозрачен, декларативен и работает на чистом Go:

r := axiom.NewRunner(...)
c := axiom.NewCase(...)
r.RunCase(t, c, func(cfg *axiom.Config) { ... })

Здесь нет неявного сканирования структур, нет DI-контейнеров, нет магии завязки на имена методов CasesXXX.

  • В Axiom рефлексия - это сбоку прикрученный адаптер, который еще и полностью опционален

  • В Testo рефлексия - это фундамент, без которого фреймворк вообще не работает (даже ваш RunTest под капотом заворачивается в синглтон-сьют и идет по reflection-пути).

Архитектурный диагноз

Вы пытаетесь представить это как набор мелких, изолированных компромиссов: тут подкрутим, там пользователь сам проверит, здесь go vet подскажет. Но если сложить все эти костыли вместе, перед нами открывается совершенно четкая и пугающая архитектурная картина)

Testo - это не "тонкий фасад" и не "пара контролируемых мест с рефлексией". Вы построили полноценный, тяжелый reflection-driven runtime, который живет поверх Go и полностью игнорирует его философию:

  • Проход по сьютам - через неявную рефлексию

  • Биндинг параметров - через угадывание имен методов (CasesXXX);

  • Инъекция плагинов - через бесцеремонное сканирование и мутацию полей структуры

  • Идентификация плагинов - через жесткий singleton-by-type, убивающий композицию

  • Сортировка плагинов - завязан на случайный порядок обхода Go-мап.

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

Тот же Axiom решает абсолютно аналогичную задачу в разы чище и честнее: через явный Runner, явный Case, явный слайс плагинов и явное получение параметров в точке использования. Компилятор все видит, компилятор все проверяет :)

Главный вывод

Знаете, почему этот спор зашел в тупик? Потому что вы написали фреймворк не для Go. Вы написали его вопреки Go.

Язык Go создавался Робом Пайком и командой именно для того, чтобы навсегда избавить индустрию от неявной магии, скрытых DI-контейнеров, рантайм-паник и развесистых enterprise-абстракций в стиле Java Spring. Инженеры уходят в Go ради предсказуемости, явности (explicit over implicit) и строгого контроля компилятора.

Вы же взяли Go и попытались насильно превратить его в Pytest или Spring. В итоге получился инструмент, который борется с собственной экосистемой.

Для внутренней автоматизации Ozon под административным ресурсом это может работать годами - спору нет, там люди подневольные. Но для независимого open-source и продакшена крупных компаний строить критически важный тестовый фундамент на рантайм-магии - это огромный, неоправданный риск.

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

Раз уж вы пришли с кодом и логами, я не поленился, залез в ваш репозиторий Testo и детально изучил исходники :) И знаете что? Все оказалось гораздо хуже, чем просто "неидиоматичная рефлексия". Вы зашили в ядро фреймворка тяжелейшие рантайм-костыли, которые прямо противоречат вашим же заявлениям о безопасности)))

Давайте разберём по фактам то, что лежит у вас в main-ветке.

1. Дырявый CI и молча пропадающие тесты (collector.go)

Вы утверждаете, что "CI точно не будет зеленым при проблемах с параметрами". Смотрим в ваш код collector.go:

if len(values) == 0 {
    fmt.Fprintf(
        os.Stderr,
        "testo: warning: ... will not run\n",
        ...
    )
    return nil
}

Что это значит? Если по какой-то причине (баг в логике, не доехавшие данные из бэка, пустой слайс окружения) ваш метод CasesOS() вернет пустой список - параметризованный тест просто молча не запустится. Он не падает, не помечается как Failed, не уходит в Skipped. Он просто тихо возвращает nil, плюнув варнингом в stderr

Для любого нормального CI варнинг в логах - это абсолютно зеленый билд. Критичный тест пропал из прогона, проверки не было, но пайплайн успешно прошел. Ваша "гарантия безопасности" сломалась в первом же файле

2. Это не "удобный фасад", это агрессивный DI-контейнер (construct.go)

Вы говорите, что рефлексия у вас "в паре контролируемых мест". На деле Testo пытается быть скрытым Spring-подобным DI-контейнером, который бесцеремонно вторгается во внутренности пользовательских структур. Смотрим в construct.go:

for i := range v.NumField() {
    field := v.Field(i)
    if !v.Type().Field(i).IsExported() {
        continue
    }
    if field.Kind() != reflect.Pointer {
        panic("testo: all exported fields in T must be pointers")
    }
    setPlugins(field.Addr(), plugins, specs)
}

Фреймворк рекурсивно обходит структуру T, сам аллоцирует указатели, сам подменяет поля и требует, чтобы все exported fields были строго указателями. Шаг влево, забыл звtздочку - получай runtime panic прямо на старте. Экспортируемые поля внутри T перестают быть обычным Go-кодом, они превращаются в жесткую рантайм-схему вашего фреймворка

3. Иллюзия гибкости плагинов

Вы заявляете, что благодаря плагинам Testo никогда не устареет. Но смотрим, как эти плагины хранятся в том же construct.go:

plugins := make(map[reflect.Type]testoplugin.Plugin, len(pluginTypes))

Ключом вашей мапы является reflect.Type. То есть ваша архитектура плагинов - это обычный Singleton-by-type Service Locator. Из-за этого два независимых экземпляра одного и того же плагина (например, с разной конфигурацией) внутри одной структуры T физически невозможно выразить. Они просто схлопнутся по типу. О какой "безграничной гибкости" тут речь?)))

В чем разница с нормальным подходом?

В том же Axiom, о котором статья, runtime-check используется локально и честно - только для проверки пользовательских данных в точке вызова: params := axiom.GetParams[LoginParams](cfg). Код прозрачен. Инженер видит, что и где он запрашивает.

Axiom не сканирует структуру тестов рефлексией, не строит неявный DI-контейнер по exported-полям, не паникует из-за отсутствия указателей и не завязывает логику плагинов на reflect.Type

Поэтому сорри, ребят, но вы облажались. Вы попытались исправить болячки allure-go, но вместо прозрачного Go-кода построили переусложненный комбайн, который держится на неявных рантайм-контрактах. Про такой софт обычно говорят: "работает — не трогай, сломалось — не починишь". Тащить это в продакшен крупных компаний - это лютый харам, и не только по-гошному, а вообще по любым канонам нормальной разработки.

За доклад на митапе все равно спасибо, но мы точно остаемся на предсказуемом Axiom

UPD: И да, ваш аргумент про "10 тысяч тестов в сотне команд внутри Ozon" не доказывает вообще ничего. В бигтехе внедрение внутренних инструментов слишком часто работает по принципу административного ресурса: сверху спустили архитектурную директиву "все новые сервисы пишем на Testo", и сотня команд берет под козырек, даже если плюется от рантайм-паник и переписывания общих хелперов. Реальное качество опенсорса проверяется свободным рынком и независимыми инженерами, а не корпоративной обязаловкой)))

Вы сами скинули цитату, которая полностью хоронит ваш же аргумент про "надежность крупных компаний". Ozon официально расписался в том, что забросил allure-go на жизнеобеспечение, потому что сами же завели его в архитектурный тупик. И их обещание "с Testo такого точно не повторится" - это чистой воды маркетинг. Еще как повторится, как только их новая неявная магия на рефлексии начнет сыпаться на объемах реальных компаний. Команду мейнтейнеров переведут на другие проекты, и они выпустят третий фреймворк. Доверия к такой "поддержке" ноль)))

По поводу рефлексии: если вам "в последнюю очередь важно" наличие compile-time проверок, вы просто никогда не ловили полностью зеленый CI в мастере при молча пропущенных критичных тестах из-за банальной опечатки в регистре имени метода (CasesOs вместо CasesOS):

Для пет-проектов и вебинаров "удобный фасад" важнее надежности, спору нет. Но для нормального продакшена сознательно ломать строгую типизацию Go в тест-рантайме - это профнепригодность. Посмотрите доклад внимательнее)

Testify топчик, сам юзаю, но https://github.com/uber-go/mock это про юниты, в статье речь про E2E / интегры

Поверьте, тут это еще вообще не магия. Вот ниже в комментах товарищ притащил ссылку на Testo - вот там реально ящик Пандоры)

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

Ага, посмотрел его - тащит абсолютно ту же самую скрытую магию в гошку. Это для Go вообще харам, спасибо, не надо) Доверие к их архитектурным решениям было полностью потеряно еще на allure-go. Уж лучше сидеть на чем-то предсказуемом, где хотя бы в рантайм прозрачный и компилятор все проверяет, чем снова дебажить чужую рефлексию)))))

Информация

В рейтинге
3 728-й
Зарегистрирован
Активность

Специализация

Инженер по автоматизации тестирования, Инженер по обеспечению качества
Старший
От 370 000 ₽
Golang
Java
Git
SQL
PostgreSQL
Linux
ООП