Как стать автором
Поиск
Написать публикацию
Обновить

Комментарии 34

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

Этот тест хрупкий потому что:

  • Зависит от внутренней реализации. Если мы решим оптимизировать код и перепишем методы db.GetProfileData и db.DeleteProfile так, чтобы они могли принимать несколько id, то нам придётся переписать и тест. Хотя логика не поменялась.

Интерфейсы плохо, потому что они мешают нам ломать обратную совместимость. Ясно. Может быть, надо просто научиться определять интерфейсы? Потому что ваша чистая функция — это и есть правильный интерфейс.

Тестирование через поведение нужно применять тогда, когда нужно протестировать поведение. 

Мне всегда нужно протестировать именно поведение. Интеграционные тесты, которые вы восхваляете, тестируют именно поведение.

Интеграционные тесты лучше тестов с моками, потому что они проверяют всю цепочку вызова […]

Ога, зато пока соседняя команда не допилит свой эндпоинт, я никаких тестов прогнать не смогу. Удобно!

———

И, наконец, вишенка на торте:

[Когда нужны моки?] — В некоторых случаях, когда нужно тестировать асинхронные операции […]

В общем, вы в 2025 году считаете асинхронные операции «некоторыми случаями» (что неудивительно в свете выбора языка). Вся остальная индустрия в то же самое время старается буквально всё сделать асинхронным.

Поэтому имеет смысл научиться правильно работать с моками, а не призывать использовать инструментарий XIX века. Так-то можно просто в прод всё выкатить, и пусть пользователи тестируют — вот уж точно лучший вариант тестов. Правда, дорогой.

У вас столько статей про функциональное программирование, кажется вы должны понимать красоту чистых функций и преимущества классического тестирования.

Интерфейсы плохо, потому что они мешают нам ломать обратную совместимость.

Нет смысла сохранять обратную совместимость между сервисом и репозиторием одного компонента в одном микросервисе. В нашем примере они просто мешают рефакторингу.

Мне всегда нужно протестировать именно поведение. Интеграционные тесты, которые вы восхваляете, тестируют именно поведение.

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

Ога, зато пока соседняя команда не допилит свой эндпоинт, я никаких тестов прогнать не смогу. Удобно!

Как часто такое случается на практике? Мне кажется это достаточно редкий кейс. Но даже в таком случае вы сможете прогнать самые главные тесты, тесты вашей бизнес-логики. Либо использовать WireMock или что-то подобное для эмулирования API которого ещё не существует, если хотите написать интеграционные тесты заранее.

В общем, вы в 2025 году считаете асинхронные операции «некоторыми случаями»

Имеется ввиду работа с очередями (кафка, рэббит). В приведённом докладе более подробно рассказан этот кейс.

Какая связь между чистыми функциями и классическим тестированием?

Как вы вашим классическим тестированием определите адекватность функции, которая выполняется в гринтреде/горутине, в полностью асинхронной высококонкурентной среде, в которой одновременно с ней выполняется еще 10К гринтредов/горутин?

Код, который выполняется в изоляции, в тепличных условиях, — можно тестировать вообще как угодно (я предпочитаю property-based тесты, они хотя бы граничные случаи найдут). Да что там, его можно вообще не тестировать, такой код синьёр обязан писать в блокноте набело со скоростью набора.

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

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

Если вам нужно протестировать как 10к функций работают параллельно и шарят состояние между собой, то это какой-то исключительный случай. Возможно, в этом случае вам действительно стоит использовать моки, я не отрицаю. Но 99% остальных тестов не такие!
Если вы приведёте мне наглядный пример где моки упрощают тестирование, то я добавлю его в статью.

и шарят состояние между собой

Это необязательное условие, они могут быть изолированы, просто работать с одними и теми же данными, не в шаред стейте, а хоть в базе/редисе.

Моки тестируют контракты, а не данную конфигурацию в данный момент времени. Код я на го я писать не стану, меня тошнит от синтаксиса, но пример привести могу, пожалуйста.

Из стороннего источника поступает поток данных (его можно получить и в тестах, если нужно) со скоростью 10К/сек. Пусть это температура воздуха в крупных городах. Удостовериться, что при запросе климатических условий в маленьком городе — интерполяция соседних городов всегда использует последние полученные значения.

они могут быть изолированы

Тогда единственное, что вам нужно протестировать это то, как ваша функция обрабатывает данные.
Нет никакой разницы работают ваши функции в горутине или нет. Одна она запущена или 10к (кроме того, что 10к это неэффективно). Используя каналы для получения данных и отправки результатов, вы можете запустить эти функции параллельно. Для Го это очень простой кейс, никакой проблемы вызванной асинхронностью тут быть не может.

for data := range inputCh {
	go func(d Data) {
		resultCh <- myFunc(d)
	}(data)
}

Ладно, вот попроще: просто удостовериться, что три асинхронных вызова происходят всегда в нужной последовательности.

У меня вся статья про то, что не нужно тестировать через поведение, нужно тестировать через проверку данных. А вы меня спрашиваете: как мне протестировать поведение?

Мок в первую очередь и верифицирует данные, а не поведение. Устраняя побочные эффекты и превращая функцию в определенном смысле в чистую.

Так зачем вам моки тогда? Пишите сразу чистые функции.

Ваша статья имеет два контекста.

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

Второй контекст, это отрицание смысла в мок-объектах, т.к. дескать поведение проверять не нужно. И это демонстрация инфантильности (детского взгляда на проблему), т.к. вы пытаетесь подменить назначение кода тестов неверным полагание их назначения, как и самого кода.

Тестирование на моках, позволяет создать контракт и описание того, как работает тестируемая система (функция, компонент, всё приложение) с точки зрения автора. Мок превращает функцию, которую невозможно превратить в чистую, в таковую.

Однако это не убирает смысла в i9n тестах против постгреса (почему я вас и просил продемонстрировать вашу способность как автора монументальных картинок в статье) на уровне юнита (модуля репозитория). Т.к. это позволит разработчику написать код на языке, который невозможно верифицировать (SQL) без языка приложения (go) в момент написания собственно модуля.

Здесь они смысл имеют и значимый. Т.к. позволяют верифицировать чистую функцию на предметном языке ( SQL). Эту функцию невозможно проверить штатными средствами её языка, а отложить её верификацию до сборки приложения целиком, может обернуться неверными эстимейтами и провалами сроков.

И вот тут важным становится мой вопрос вам в треде ниже - как запускать группы таких тестов за критически малое время с идемпотентным результатом и у разработчика и в CI.

Если верификационная группа будет у вас 5 минут работать только на уровне 1 юнита, то на уровне всего приложения 500-700 тестов у вас будут выполняться половину дня. И такого рода тесты становятся БЕСПОЛЕЗНЫМИ для разработчика.

Ну и напоследок. Го (особенно в контексте ваших же диаграмм в статье), это язык про поведение и побочные эффекты. Тестирование данных бессмысленно как таково. Любая функция с побочным эффектом, это всегда - поведение.

Именно поэтому данные (и их декларации в виде описаний структур) это существительные. А методы (или функции) это глаголы. Поэтому тестируя любой действующий юнит - вы тестируете в первую очередь поведение. А верифицируете его через результат ЕГО жизнедеятельности - в виде side effect.

Поэтому финальная часть вашей статьи - это бессмыслица.

Это блистательный комментарий, с которым я, разумеется, полностью согласен, но в одном месте вы в пылу полемичного задора увлеклись :)

И такого рода тесты становятся БЕСПОЛЕЗНЫМИ для разработчика.

Разработчик обычно запускает свой тест, на тот кусок кода, над которым он работает. Из этого не следует, что тесты должны выполняться по пять минут, но многие разработчики терпят свои пять минут. Если бы нельзя было запускать тесты выборочно — только пакетом — все бы давно научились писать быстрые тесты.

Это ограничит всё же возможности использования отладчика, а это неоспоримый плюс TDD. Особенно в го.

Во-первых, я же не сказал: «надо было запретить», я сказал: «если бы было запрещено». Во-вторых, я, если честно, не очень понимаю, зачем вообще пользоваться отладчиком, когда можно отгранулярить тесты. В-третьих, в высококонкурентной среде — отладчик так себе помощник.

В golang нет проблем с интерфейсами, благодаря duck-typing.

Статья высосана из пальца.

Спасибо за статью.

Подчерпнул для себя много нового.

Когда я вижу очередную статью или видеоурок про тестирование кода, я почти уверен, что мне опять расскажут про моки.

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

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

Спасибо за статью.

Странно, что в списке литературы нет Принципов Юнит-тестирования Владимира Хорикова. Как будто конспект прочитал. Кто хочет более фундаментальные знания по данному подходу очень рекомендую к прочтению.

Покажите мне пример интеграционного теста на golang, функции использующей SQL запрос в Postgresql (первый же ваш пример с кодом) так, чтобы я его запустил одной командой go test, со скоростью исполнения до 100 миллисекунд. Ну и конечно, запрос к таблице которая при этом создаётся с помощью миграции. И конечно эти тесты должны быть изолируемы и исполняться в последствии на CI максимум с использованием только докера.

Раз уж вы так пропагандируете интеграционные тесты без моков, наверняка покажете. И к слову, это должно быть регулярно используемое решение. А не полотно на 500 строк как обычно в го принято.


Потому что я пока вижу у вас только критику мока как инструмента и рекомендацию использовать то, чего в статье - нет.

Я просто вставил ваш комментарий в ChatGPT и он сгенерил рабочий тест.
https://gist.github.com/AlexAkulov/12644f01ef36d5faade8b3cedd86fbe8
Всё как вы хотели, но на моём М1 отрабатывает за 3 секунды.

Я упомянул в статье https://testcontainers.com -- отличная штука, если не знаете с чего начать.

Код вашего GPTChat линейно скалируется по времени от числа тестов. У меня есть пакеты с постгресом где 100 тестов отрабатывают примерно 0.6 секунды. В вашем случае с вашим примером эти замечательные тесты отработают за 300 секунд. Выглядит как антиреклама тестированию.

Я бы не задавал вопрос, если бы не знал ответ. Но вот в статье где РЕКОМЕДНУЮТ не использовать моки им уделено много времени, а вот демонстрации силы интеграции - нет.

Ну ок, пусть работают 300 секунд, но найдут ошибку, чем отработают за секунду, ничего не найдут, а баг обнаружится после деплоя.

Ну т.е. 100 регулярных тестов (модульных) с интеграцией на 30-40 методов репозитория вы будете натурально выполнять 5 минут и считаете это нормальным.

Правильно ли я понимаю, что вы не знаете как сделать такию группу тестов работающей за обозримое время в пормежутке до 4 секунд (максимально комфортное время исполнения любой группы тестов. после которого они начинают вредить вредить и замедлять работу)?

В каких-то языках просто компиляция проекта занимает сильно больше чем 5 минут. И как-то люди живут с этим)

У вас есть множество способов ускорить интеграционные тесты. Вы можете не удалять контейнер с Постгресом каждый раз, а просто делать drop database. Вы можете запустить БД по числу ядер и прогонять тесты параллельно. Тут большой простор для оптимизаций, но я вообще не вижу в этом проблемы. Вроде у всех уже давно есть CI/CD, просто пушишь изменения и идёшь пить чай, сколько там эти тесты прогоняются 5 секунд или 5 минут вообще не важно.

У нас крайне странный диалог выходит.

Я задал вам параметризированный вопрос. Ответ и решение которого у меня есть (в виде специализированной библиотеки: https://github.com/godepo/pgrx).

Вы же не предлагаете никакого рецепта и лишь общение с чатгпт.

Если бы я хотел поговорить с низкоквалифицированным специалистом, вместо эксперта с креативным мышлением. Я бы с этим справился без комментариев на хабре :)

Но пить чай за счёт работодетля, конечно удобно. Спорить не буду.

Я не понимаю, вы таким сложным образом просто хотели посоветовать https://github.com/godepo/pgrx ?
Хм, ну ок, спасибо!)

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

Т.к. написав статью с развенчанием одного из подходов и рекомендацией другого, вы выступили экспертом, мне было любопытно, как вы решаете обозначенную мной проблему - т.к. она влияет на time to market непосредственно.

Т.к. я эту задачу решал и пропогандировал TDD в команде, мне было интересно как вы решили проблему i9n тестов. Т.к. они у вас панацея от всех бед. Т.к. моё решение, может быть не идеальным.

Негипотетический финтех сервис, поддерживаемый 3 разработчиками за полгода оброс 1200 тестами. Вы предложили этой команде убрать все моки и заменить их - интеграционными тестами. Каждый из которых будет линейно исполняться 5 секунд (суммарно 100 минут - 3 часа+).

Часовая авария обойдется компании в 2 миллиона долларов неполученной прибыли. Вы предложили. потратить 6 миллионов долларов на деплой quick fix'a. Лучшие решения, что я видел в исполнении других пропогандистов i9n-only тестирования работали примерно такое же количество времени.

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

Прочитайте, пожалуйста, статью ещё раз. Там не только про то, чтобы заменить все тесты на интеграционные.

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

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

В третьих. Количество интеграционных тестов зависит от количества эндпоинтов. В среднем микросервисе их скорее десятки, а не сотни. Соответственно и интеграционных тестов у вас будет пропорционально.
Так же я написал, что у вас есть множество способов оптимизировать выполнение интеграционных тестов. Закладывать 5 секунд на прогон одного теста - это слишком большое допущение. Вот у меня есть проект где около 70 интеграционных тестов и они прогоняются за 16 секунд. Если бы я захотел их улучшить, то в первую очередь я бы сократил их количество, а не скорость выполнения.
Go, кстати, умеет кэшировать результаты выполнения тестов, и запускать только нужные, можете это тоже настроить в CI.

По итогу ваши расчётные 3 часа превращаются в минуты или десятки секунд.

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

В нашем примере один интеграционный тест покрывает код всех компонентов, тестирует их взаимодействие, а также код middlewares, ORM и реальные sql-запросы в реальной базе данных. Такую глубину тестирования невозможно было бы достичь, если бы мы использовали тесты с моками.

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

А одна из ваших целей - иметь быстрые тесты. Правда под их "скоростью" вы подразумеваете:

  • Быстрыми — чтобы их удобно было запускать локально и в CI.

А уже в комментариях со мной утверждаете, что 5 секунд это быстро, потому что где-то ещё дольше.

Количество i9n тестов зависит не от эндпоинтов, а от комбинаторной сложности входных параметров и числа поведенческих сценариев тестируемой системы.

Вообще главный критерий числа тестов - это цикломатическая сложность тестируемой системы. Это вообще минимальный набор тестов который у вас должен быть.

 Вот у меня есть проект где около 70 интеграционных тестов и они прогоняются за 16 секунд. Если бы я захотел их улучшить, то в первую очередь я бы сократил их количество, а не скорость выполнения.

И как же вам поможет это сокращение? Вот их ускорение до 2 секунд очевидно что даст - скорость итерации процесса Green-Red-Refactoring.

P.S. Джим Коплинг не может быть аппеляцией к авторитету. У него нет авторитета в обсуждаемой области никакого, кроме работы в консалтинге. Ну и для меня ни он, ни его книги не авторитетны.

Количество i9n тестов зависит не от эндпоинтов, а от комбинаторной сложности входных параметров и числа поведенческих сценариев тестируемой системы.

Вы правы в этом уточнении.

Вот их ускорение до 2 секунд...

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

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

Недавно тоже пришел к такому выводу. Тесты с моками очень хрупкие. Ведь любой продукт меняется и эволюционирует каждый день. Каждый раз править моки становится очень сложно. Поэтому интеграционное тестирование и юнит тесты для меня оказались лучше. Например у меня есть три входные точки в приложении - grapqhql, http, cli. Есть слой api а под ним кучу разных слоев такие как репозиторий, внешние api, redis, очереди. Я запускаю интеграционный тест который в отдельной базе данных уничтожает все данные. Заново наполняет в соотвествии с условиями тестов и дергает api. Затем проверяю так ли должен ответить метод api при таких входных данных. Такие тесты не ломаются при каждом чихе и можно спокойно рефакторить.

А ваши замечательные тесты могут работать все одновременно?

Юнит тесты да. Но интеграционные нет. Но там очень быстро все происходит. Благо сейчас компьютеры мощные. Лучше подожду пару секунд чем буду тратить часы на переделку моков

Зарегистрируйтесь на Хабре, чтобы оставить комментарий