The post has been translated automatically. Original language: Russian
Technical debt is discussed either as a disaster or as something that will be thought about later. Both are incorrect. Debt is a normal tool: you take speed now in exchange for additional work later. The problem starts where no one counts the percentages. Debt can be different, and decisions are also different. A conscious duty. We know that the solution is temporary, we know why, and we know when we will return. This is the healthiest option.An unconscious duty. We did our best, and then we found out what was better. He learns by experience, but is not treated by discipline.Debt from changed requirements. The code was correct for the previous task. It's not anyone's fault, but you'll have to rewrite it anyway.Debt from negligence. They knew how to do it, they did it differently, they didn't write it down. The only type who is not justified by anything.It is useless to mix them in one conversation: the first type is planned, the second trains the team, the third is inevitable, and the fourth needs to be stopped by the process. How to understand that debt has become expensive The evaluation of simple tasks began to grow. A change in one place requires edits in the other three. There are sections of the code that no one but one person wants to touch. Bugfixes regularly lead to new bugs.Onboarding a new developer takes weeks instead of days. The team increasingly responds, "it's easier to rewrite than to fix." The third and fifth signs are the most significant. They mean that no one has the whole system in their head anymore. What really worksWrite down the debt in the same place where the tasks live, and not in a separate forgotten document. An unfixed debt becomes folklore after six months. Link refactoring to tasks that are already included in this code. A separate "sprint for technical debt" almost always loses the fight against priorities. Formulate the cost in the language of business. Not "bad architecture", but "each new integration takes three weeks instead of three days". Distinguish between debt in hot and dead areas. The perfect code in a module that no one has touched for two years is not needed by anyone. To agree that part of the debt remains forever. Giving it all back is also a form of perfectionism. The main idea It is not the duty itself that is dangerous, but its invisibility. A team that knows where its corners are cut and how much it costs manages the situation. The team that finds out about this at the moment of production failure only reacts. And how do you record the technical debt: in the backlog, in the comments to the code, or is it based on the memory of the team?
Покрытие 85%, зелёный пайплайн, релиз выходит и сразу ломается. Ситуация обычная и она не про лень команды. Тесты проверяли ровно то, что было легко проверить, а не то, что действительно могло сломаться.
Почему покрытие обманывает
Покрытие показывает, что строка кода выполнилась во время теста. Оно ничего не говорит о том, проверил ли тест результат. Тест, который вызывает функцию и не смотрит на её вывод, даёт то же самое покрытие, что и осмысленная проверка. Отсюда и разрыв между красивой цифрой и падающим продакшеном.
Что обычно остаётся непроверенным
Граничные значения. Пустой список, ноль, отрицательное число, строка максимальной длины. Основной сценарий тестируют почти всегда, границы почти никогда. Поведение при ошибке внешнего сервиса. Что происходит при таймауте, при ответе 500, при некорректном JSON. В тестах мок всегда отвечает успешно.Повторный запуск. Обработчик вызвали дважды подряд, потому что сообщение пришло дважды. Проверяют это редко, а в очередях это норма. Одновременный доступ. Два запроса меняют одну запись. Тест выполняется последовательно и такую ситуацию не воспроизводит. Миграция состояния. Код работает на новых данных, но в базе есть записи, созданные полгода назад по другим правилам.
Что делает набор тестов надёжным
Много быстрых юнит-тестов на логику, меньше интеграционных на стыки, совсем немного сквозных на ключевые пользовательские пути. Обратная пропорция даёт долгий и хрупкий пайплайн. Каждый тест проверяет одно поведение и по имени понятно, какое именно. Название вида test_user_2 не помогает никому. Тесты независимы и не зависят от порядка запуска. Если они падают только вместе, проблема в общем состоянии между ними. Никакой реальной сети и реального времени. Обращение к внешнему API делает тест нестабильным, а зависимость от текущей даты приводит к падению в первый день месяца. Каждый пойманный в продакшене баг превращается в тест до того, как его исправят. Это единственный способ гарантировать, что он не вернётся.
Про нестабильные тесты
Тест, который падает раз в десять запусков, хуже отсутствующего. Команда быстро привыкает перезапускать пайплайн вместо разбора, и вместе с ложными падениями начинает игнорировать настоящие. Такой тест нужно либо починить, либо отключить и записать как долг, но не оставлять в подвешенном состоянии.
Чем полезнее мерить
Вместо процента покрытия смотрите на две вещи: сколько багов дошло до продакшена за последний месяц и сколько из них теперь покрыто тестом. Это неудобные цифры, зато они про реальность, а не про строки кода. А у вас есть практика заводить тест на каждый найденный баг, или чаще ограничиваетесь исправлением?