Comments 23
То есть простой вызов функции в тесте уже подойдет, чтобы “покрыть” эту функцию.
Это странно. Обычно считается вызов строк внутри самой функции. А вызов функции, это просто учет строки где функция вызывается и не говорит о том, что функцию "покрыли".
У себя в команде при описании задачи в разаработку там же прописываю сценарии проверки имплементации. Разработчики уже знают и пишут по ним сразу тесты.
На коммит-ревью видишь и изменения и как эти изменения тестируются - сильно проще понимать всё ли нормально.
По началу тимлиды ленились и бунтовали делать нормальную постановку задач. Но по результатам эксперимента в два релиза они сами втянулись.
По результатам подсчета метрик эти команды самые эффективные с точки зрения сроков исполнения, количества закрытых тикетов на количество багов и reopen.
Про вызов функции - спасибо за замечание, и правда неудачная формулировка.
У вас очень хороший подход, так и разработчик сразу видит более точные требования и оценка задаче будет более честная:) А ресурсов QA хватает на все задачки?
Хорошо бы ещё научиться понимать какой функционал годится для проверки юнит-тестами, а какой нет :)
Метод, который мы тестируем, не только возвращает информацию о пользователе, но и обновляет состояние системы
Иногда тесты даже подсказывают, даже кричат "что за хрень ты написал???" В данном случае метод заметно нарушает модные принципы дизайна, о которых так любят спрашивать на собеседованиях, но напрочь забывают непосредственно при разработке - он и в базу лезет дублируя функционал db.GetUser(...)
- это более низкий уровень и мутирует переданный параметр по ходу. Да, по логике разработчика "так задумано". Но тест выглядит подозрительно: он тупо повторяет код. Он не проверяет логику, он проверяет что код написан тестировщиком и программистом одинаково.
Затрудняюсь сказать "как должно быть" т.к. не знаю деталей задачи - возможно это абсолютно вымышленный тестовый пример и такой вопрос вообще не стоит. Но в общем хочется подчеркнуть - к тестам надо прислушиваться - они порой подсказывают что надо код порефакторить :)
А как мутационные тесты в Go делать?
У нас используется доработка этой библиотеки go-mutesting
Тут форк, который можно использовать - https://github.com/avito-tech/go-mutesting
Что у неё по скорости работы?
Я в паре своих питонячьих проектов использую mutmut
, и у него со скоростью всё достаточно грустно. Если в норме все тесты пробегают за несколько секунд, то при запуске мутатора можно идти пить кофе. Оно и неудивительно: для проверки условных 500 мутантов (проект небольшой) требуется запустить все тесты до 500 раз (до первого падения). Метод реально хорош, но к скорости тестов весьма чувствителен.
Как у вас с длительностью дело обстоит, если не секрет? Не слишком ли удлинняется пайплайн?
Пожалуйста, выкатите статью о применении go-mutesting в практике на реальных кейсах и как внедрили в процессы конвейерной разработки
Есть такая статья на эту тему, надеюсь, будет полезна - Мутационное тестирование: опыт внедрения на 1500 сервисов
Первое - блокер в пайплайне требующий покрытия тестами без возможности override - есть чистое зло. Ибо девелоперы будут писать тесты которые ставят своей целью "потрогать" лежащий код - и вы потеряете ценный диагностический критерий. В общем - это опять классическая ошибка менеджмента: диагностический критерий сделать целевым. Низкий процент покрытия должен трансформироваться в задачу в бэклоге - проанализировать компонент, и определить сценарии которые сейчас не покрыты тестами. А потом в задачи - покрыть эти сценарии. Вместо этого, менеджмент срезает угол и ставит задачу: увеличить покрытие тестами - а еще со времен советской армии известно: как задача поставлена, так она и выполняется...
Второе - спорное ИМХО, но я пришел к выводу что если в вашем юнит-тесте есть моки, то или у нас проблема с архитектурой, или это не юнит-тест. Юнит-тест должен тестировать конкретный нетривиальный алгоритм. Вот например если у нас в коде есть метод расчета контрольной цифры EAN13 штрих-кода - его можно тестировать юнит-тестом. А когда мы начинаем мокать базу данных - ну такое себе... Юнит тесты в этом случае начинают вырождаться в психиатрический тест: что наши предположения по поводу кода в момент его написания соответствуют нашим же предположениям в момент тестирования. Нет, я не спорю - иметь подтверждение что разработчики не страдают раздвоением личности - это неплохо для бизнеса. Но не то, чего бы нам хотелось на самом деле...
Третье - сама идея юнит-тестов рождалась во времена когда мы писали 90% кода, и 10% использовали библиотек. Сейчас, наши приложения - это небольшие плагины, которые кастомизируют поведение гигантских фреймворков. Если ваш тест не запускает эти скрытые слои кода и не проверяет что вы их правильно кастомизировали - он имеет мало отношения к реальной жизни. Я встречал приложение с идеально зелеными тестами, которое в реальности даже не запускалось (забыли миграции БД положить). И толку в таком тестировании ?! Если мы хотим работающее приложение - надо заводить тест-контейнеры и заменять тучу юнит-тестов с моками - нормальными интеграционными и компонентными тестами.
Четвертое - компонентное и интеграционное тестирование нами в теоретическом и практическом смысле поняты намного хуже юнит-тестирования. Особенно - как его сделать разумным с точки зрения стоимости написания и поддержки. Но, по моему опыту - за этим скорее всего будущее...
если в вашем юнит-тесте есть моки, то или у нас проблема с архитектурой, или это не юнит-тест.
спорное утверждение, имхо
Я не настаиваю, и даже в моем ИМХО может быть много исключений. Но в реальности - когда люди мокают БД или репозиторий - оно потом берет, и не работает! Потому что аннотации Transactional расставили неправильно. Потому что Hibernate словил на границе транзакционного блока эксепшн, и пометил всю транзакцию как roll-back only. Потому что Lazy loading proxy не может материализоваться из-за рано закрытой сессии, и так далее. И даже H2 в качестве БД для тестов - ведет себя не так как работает реальный Постгрес или Оракл. Соответственно - если мы тестим БД, а не делаем вид - значит добро пожаловать в testcontainers...
А если мы не тестируем БД в юнит-тесте - тогда объясните, зачем ее мокать ?! Если вам нужен для юнит-теста какой-то объект, не надо изображать что вы его получили из БД - создайте руками через билдер или конструктор то что вам нужно для юнит-теста, и протестируйте. А вот если у вас приложение написано так, что вы не можете добраться до тестируемого алгоритма потому что там в коде подход винтика-шпунтика "нам нужен пылесос - так давайте же наделим его еще и функциональностью холодильника!" - то есть один и тот же метод и извлекает объект, и проводит над ним нетривиальные преобразования - то это именно то, что я называю проблемой архитектуры. По науке, нужно бить функционал на извлечение и на преобразование (и юнит-тестом покрывать второе, но не первое). Но если времени нет, архитектуру менять поздно - тогда начинаются танцы с моками...
А если мы не тестируем БД в юнит-тесте - тогда объясните, зачем ее мокать ?!
Мне кажется, вы обманываете сами себя. Хотите вы этого или не хотите, но каждый тест явно или неявно тестирует все зависимости, которые создаются при его инициализации. Поменялась версия драйвера БД, обновилась версия СУБД, поменялась реализация ORM библиотеки для работы с БД - все это потенциально может привести потом к падению любого теста, в котором есть даже глубоко вложенная зависимость на реальную БД.
И вот с такой логикой как у вас потом возникают проекты с десятками и сотнями тестов, которые явно или неявно тестируют ВООБЩЕ ВСЕ - реальную БД, реальные HTTP вызовы, реальные вызовы к брокерам сообщений. И авторы таких проектов гордо называют все это юнит-тестами, хотя никакие это не юнит тесты, а интеграционные. Я уже имел печальный шанс переписывать такие проекты - вносить какие-либо изменения с такими тестами это то еще "удовольствие".
К тому же такой подход поощряет написание не качественного и тестируемого кода, где мы внедряем зависимости и можем их подменить, а абы какого кода, где зависимости инициализируются прямо в конструкторах - ведь зачем утруждаться, в тестах ведь не нужно ничего мокать.
Так что если у вас обращение к БД (HTTP/Kafka, подставить нужное) не вынесено в отдельный слой, а пронизывает весь код так, что его невозможно нормально протестировать без использования реальной БД, так это вопросы к качеству кода, а не "моки это вредно". Обратитесь к истокам, вспомните, что такое "D" в SOLID, что такое тестируемый код, и все встанет на свои места.
И если нужно будет протестировать слой работы с БД, слой работы с HTTP, слой работы с брокером сообщений, можно написать отдельный небольшой набор интеграционных тестов, которые тестируют именно взаимодействие, а не логику. А в юнит тестах все такие внепроцессные зависимости подменять.
Возвращаясь к первоначальной процитированной фразе, перефразирую ее с точностью до наоборот - если мы явно не тестируем в юнит тесте БД - зачем нам там зависимость на реальную БД?
Я же не предлагаю заменить юнит-тесты - интеграционными и компонентными! Для меня идеальный юнит-тест - это создали входной объект(ы), скормили в тестируемый метод - получили объект-результат, заассертили его. Еще раз приведу пример про алгоритм вычисления контрольной цифры штрих-кода EAN13 - этой части программы пофиг на то, что на самом деле EAN13 это признак SKU который на самом деле привязан к корпоративному справочнику товаров. Задача тестируемого метода - получить 12 знаков штрих-кода и правильно рассчитать 13-й. И вот это мы тестируем юнит-тестом.
А моки у нас появляюся когда ? Когда алгоритм вычисления принимает на вход SKU, сам лезет в базу и у вытащенного значения считает контрольный разряд. Но тут я с вами соглашусь - это нарушение SOLID - и поэтому начинается страдание над моками.
Соответственно, моя логика простая - если вы в коде юнит-теста что-то мокаете, значит вы взялись тестировать слишком большой кусок кода. Разбейте на части которые не требуют моков и затестируйте. Это не относится, разумеется, к ситуациям где мы мокаем методы для того чтобы спровоцировать ошибку, например. То есть мок, который кидает эксепшн требуемого типа я считаю вполне нормальным, экстремизма в стороны вычищения всех моков тоже не следует допускать.
Что касается юнит-тестов из серии "давайте ткнем слой логики, и проверим что он сходит в БД и возьмет данные" - я считаю их преимущественно вредными. Любой интеграционный, а еще лучше - компонентный тест вам как побочный эффект прекрасно проверит - ходит ваш слой логики в БД, или не ходит...
В сухом итоге - я считаю правильным сочетание юнит-тестов в их изначальном понимании (тестировании небольших изолированных частей системы), и потом тесты всего компонента (включая конфигурацию, тест-контейнеры с кафкой и базой данных) на бизнес-сторях подготовленных BA. Потому что если у вас изменится сторя - вы и так и этак будете тесты переделывать.
А большой ошибкой я считаю обматывать приложение тестами бездумно, по-принципу "больше бумаги - чище задница!". Тогда фактически, тесты перестают покрывать функционал - "что!" должно быть сделано, и вместо этого начинают тестировать структуру и связи внутри приложения - "как!" должно быть сделано: вызывает ли этот метод - вот тот ?; ходит ли оно в базу данных ?;принимает ли этот метод ровно три параметра ? - и потом получается структура приложения которую невозможно изменить с сохранением функционала: где не потрогай, половина тестов краснеет.
К сожалению, у вас именно тот случай, когда зациклившись на чем-то, уже никакие аргументы не будут иметь веса в дискуссии. По сути, вы предлагаете писать юнит-тесты только на какое-то очень ограниченное подмножество частей кода, не имеющих зависимостей - типа перемножателей чиселок. А всю остальную бизнес-логику (которой обычно большинство), предлагаете тестировать тяжелыми интеграционными тестами "на бизнес-сторях".
Уже по своей сути такая концепция это антипаттерн, и не укладывается хотя бы в классическое понятие пирамиды тестов. Кроме того, такие тесты будут очень долго запускаться, такими тестами будет очень сложно охватить все возможные сценарии каждого участка кода, такие тесты будет очень сложно отлаживать в случае падения, потому что они могут упасть по куче причин. И так далее, и тому подобное.
тесты перестают покрывать функционал - "что!" должно быть сделано, и вместо этого начинают тестировать структуру и связи внутри приложения - "как!" должно быть сделано: вызывает ли этот метод - вот тот ?; ходит ли оно в базу данных ?;принимает ли этот метод ровно три параметра ? - и потом получается структура приложения которую невозможно изменить с сохранением функционала: где не потрогай, половина тестов краснеет.
С этим соглашусь. Так, конечно, делать не надо. Но это никак не связано с наличием или отсутствием моков. Никто не обязывает при использовании моков проверять, сколько раз был вызван метод и с какими параметрами - моки всего лишь позволяют обеспечить изолированное тестирование кода. И если такой тест упадет, то только по единственной причине - значит, сломался тестируемый код. А не какая-то из 10 зависимостей.
Я же не на ровном месте это говорю - по итогам создания и долговременной эксплуатации систем (в том числе, внесения в них изменений). Юнит тесты, в классическом понимании - тестирующие корректность поведения нетривиальных алгоритмов - безусловно, полезны. Более того, чем ниже в слоях кода лежат эти алгоритмы - тем более важно для них покрытие юнит-тестами. Потому что этот код прямо или косвенно будет переиспользован приложением многократно! Соответственно, я хочу быть уверен что эти кирпичи в основании - они нормальные.
А теперь внимание - вопрос: что в вашем понимании есть "бизнес-логика" ? Если бизнес-логика - это "получить входящий REST-call, сходить в базу и извлечь сущность, изменить ее, записать обратно, вернуть response-entity" - то я категорически против того, чтобы это тестировать unit-тестами. Здесь юнит-тест должен покрыть единственное нетривиальное действие "изменить ее". Все остальное - не тестирует бизнес-логику, а тестирует структуру приложения: вызывается ли репозиторий из контроллера, вызывается ли код изменения, и так далее... В моей практике - эти тесты ничего не дают, но их поддержка постоянно стоит денег.
Более того - сложная "бизнес-логика" в большой системе имеет свойство быть кастомизированной. Пример - в большой системе есть метод resolveBarcode который пытается по входящей строке символов определить, является ли это корректным штрих-кодом, и если да - то чего именно ? Как он реализован ? А разумеется через chain-of-responsibility, которая последовательно вызывает частные распознаватели штрих-кодов начиная от общеупотребительных типа EAN13 или ITF14 до внутренних кодировок предприятий-партнеров. А где задается порядок и состав делегатов-опознавателей ? Правильно - в конфиге. Следовательно, если вы не делаете компонентный тест - спринг-бут вам не поднимет контекст. А нет контекста - нет бинов-делегатов в resolveBarcode. И какую "бизнес-логику" вы хотите тут проверить юнит-тестом ?! Индивидуальные делегаты и алгоритмы в них - понятное дело тестами покрыты. А то, что из индивидуально правильных кирпичей построено правильное здание - проверяется компонентными тестами. Более того, в разных проектах эти тесты еще и разные - потому что разные клиенты хотят одним и тем же кодом реализовать - сюрприз! - разную бизнес-логику!
Соответственно по мокам - моя позиция такая, что это наличие мока в тесте - это скорее всего (не впадая в истерику и экстремизм) - признак проблемы. Либо у вас код так написан, что вы не можете изолировать execution path для юнит-теста, либо вы тестируете слишком большой кусок кода, либо на самом деле вы не хотите мок, а вы хотите проверить взаимодействие частей системы - и для этого вам лучше подойдет тест где все эти части будут реально присутствовать в конфигурации похожей на реальную. Потому что юнит-тесты по своей природе очень плохо предназначены для отслеживания побочных эффектов stateful кода. А как только execution path уходит в дебри spring-boot, hibernate и товарищей - это оно и есть. И побочные эффекты их состояний влияют на ваш код. Поэтому (если вам нужна гарантия что приложение заработает у клиента) - вы все-равно будете тестировать его компонентными и интеграционными тестами чтобы убедиться что разные случайности и неприятность во взаимодействии с фреймворком его не роняют. А если так - то зачем нужен слой юнит-тестов, которые принципиально тестируют то же самое, только с моками ?
И нет - парадигма пирамиды тестирования этим не нарушается. Юнит-тестов всегда сильно больше, чем компонентных. И да, если вы пишите и потом поддерживаете тесты не за свои деньги - а за это платит кто-то другой, то можно еще и юнит-тестов намазать с моками. А вот если это свои деньги - наступает вопрос о разумной достаточности...
Тут допускаю, что опыт у всех разный
Насчет юнит и интеграционных тестов - это все же две разные сущности, которые не исключают друг друга. Более того, юнит тесты позволят покрыть, условно, 500 сценариев бизнес-логики без похода в базу, что сильно облегчит разработку и избавит от необходимости поднимать каждый раз весь контур.
Интеграции - это очень важно и их обязательно нужно проверять, вы правы, но с наличием юнит тестов вы сильно сократите количество интеграционных (в этом примере потребуется добавить только кейсы на запись в бд)
Я не против юнит-тестов как таковых. Напротив - за. Но без моков. И в последнее время я начинаю быть против того, чтобы каждый разработчик себе создавал тестовые объекты для юнит-теста. Потому что потом начинатся зоопарк, и у каждого человека оказываются свои представления о том, как на самом деле будут выглядеть объекты с которыми мы должны работать. Последние два-три проекта я начал внедрять в практику "справочник тестовых объектов", за которые в конечном счете отвечают не разработчики и QA+BA (ну или да, сам разработчик - если он для этого модуля сам-себе QA+BA). Смысл в том, что даже для юнит-теста, мы берем заготовленный (корректный с точки зрения бизнеса) объект, кастомизируем его чуть-чуть если нужно (чтобы избежать экспоненциального разрастания числа тестовых объектов в справочнике) - и уже то что получилось используем как вход в тесте. Иногда это позволяет выловить нетривиальные ошибки и взаимные влияния (которые бы не проявились если бы разработчик тупо забил поле которое его не интересовало null-ом, или значением "от балды").
Спасибо за статью!
В рамках темы мне очень зашла книга Владимира Хорикова «Принципы юнит-тестирования» - зашли идеи главенства подсчета покрытия не строк кода, а ветвлений, плюс по полкам раскладывается про тестируемость кода и способы ее повышения/обеспечения.
Спасибо за статью, очень информативно!
Есть один вопрос:
Я начинающий разработчик и сейчас поднимаю покрытие тестами в проекте. Можно ли использовать gomock.Any для контекста?
Как писать качественные unit-тесты: процент покрытия, мутанты и работа с моками