За последние десять лет роль тестирования изменилась гораздо сильнее, чем за предыдущие двадцать. Если раньше QA воспринимался как последний рубеж перед релизом, то сегодня инженеры по качеству участвуют в проектировании требований, помогают команде выстраивать процесс поставки изменений и зачастую становятся теми, кто первым замечает, что система начинает терять устойчивость.
При этом вокруг технического долга до сих пор существует одна довольно устойчивая иллюзия. Стоит в команде заговорить о замедлении разработки, росте количества регрессий или усложнении релизов, как почти неизбежно звучит предложение: «Нужно усилить тестирование». Иногда под этим понимают автоматизацию, иногда — увеличение покрытия тестами, иногда — дополнительные проверки перед релизом. Формулировки могут отличаться, но логика остаётся одной и той же: если система становится сложнее, значит, её нужно лучше тестировать.
На первый взгляд такая логика выглядит вполне разумной: если система становится сложнее, значит, её нужно лучше тестировать. Проблема в том, что тестирование не устраняет причину этой сложности. Оно лишь позволяет раньше заметить её последствия.
Мы привыкли воспринимать технический долг как инженерскую проблему, которую можно постепенно уменьшить: провести рефакторинг, переписать несколько сервисов, улучшить покрытие тестами. Но тестирование вообще не находится в той точке жизненного цикла, где этот долг появляется. Оно сталкивается с ним значительно позже, когда архитектурные решения уже приняты, зависимости между компонентами сформированы, а сложность системы стала объективной характеристикой продукта.
Поэтому главный тезис этой статьи довольно простой, хотя и не всегда очевидный.
QA не уменьшает технический долг. Его задача гораздо важнее — сделать этот долг наблюдаемым.
Именно вокруг этой мысли дальше и будет построен разговор.
Технический долг — это не код
Когда инженеры обсуждают технический долг, разговор почти всегда очень быстро сводится к качеству исходного кода. Вспоминают большие классы, циклические зависимости, сервисы, которые никто не хочет трогать перед релизом, или методы длиной в несколько сотен строк. Всё это действительно существует и, безусловно, усложняет сопровождение продукта. Но каждый раз, когда я слышу подобные примеры, мне кажется, что мы обсуждаем последствия, а не саму проблему.
Попробуем посмотреть на ситуацию немного иначе.
Представьте две системы. Обе написаны примерно на одном технологическом стеке, сопоставимы по размеру, проходят одинаковый набор функциональных тестов и даже имеют близкое покрытие кода. Если открыть репозитории рядом, нельзя сразу сказать, какая из них окажется более дорогой в сопровождении.
Разница становится заметна только тогда, когда появляется необходимость внести изменения.
В первой системе разработчик получает задачу утром, к вечеру открывает pull request (запрос на слияние изменений), а на следующий день изменение уже проходит регрессию и уходит в релиз. Во второй аналогичная задача неожиданно превращается в небольшое исследование. Сначала приходится разбираться, какие сервисы могут оказаться затронутыми. Затем выясняется, что изменение влияет ещё на несколько интеграций, о которых никто не вспомнил на этапе оценки. После этого QA закладывает дополнительное время на регрессию, потому что похожие изменения уже приводили к неожиданным побочным эффектам. Сам код при этом может оказаться вполне аккуратным.
Поэтому я бы не стал рассматривать технический долг исключительно как характеристику исходного кода. Код лишь делает накопленную сложность заметной, тогда как сама проблема проявляется значительно раньше — в свойствах инженерной системы и стоимости её изменений.
В какой-то момент команда начинает тратить всё больше времени не на разработку новой функциональности, а на анализ последствий изменений. Любая доработка требует более длительного проектирования, более объёмной проверки и более осторожного релиза. Если подобная ситуация становится нормой, технический долг уже существует, даже если количество дефектов остаётся невысоким.
Несколько лет назад мне довелось работать с системой, в которой разработчики долго не могли объяснить, почему скорость команды постепенно снижается. Количество задач в спринте почти не менялось, серьёзных инцидентов не происходило, а число дефектов даже уменьшалось. При этом каждая новая задача неожиданно требовала всё больше времени на анализ. Разработчик открывал тикет и вместо того, чтобы писать код, первые полдня пытался понять, какие сервисы вообще окажутся затронутыми изменением.
Любопытно, что проблема оказалась не в коде как таковом. За несколько лет система постепенно обросла десятками интеграций, часть которых уже никто не воспринимал как риск. Каждое отдельное изменение выглядело вполне разумным, но их накопительный эффект оказался значительно сильнее любого отдельно взятого архитектурного решения.
Архитектурные решения
↓
Свойства инженерной системы
↓
Стоимость изменений возрастает
↓
QA начинает фиксировать последствия
Технический долг бывает разным
Когда говорят о техническом долге, создаётся впечатление, будто речь идёт об одном явлении. На практике всё значительно сложнее. Система редко деградирует только в одном направлении. Гораздо чаще разные виды долга начинают усиливать друг друга, создавая тот самый эффект, который разработчики описывают фразой «что-то стало очень тяжело менять».
Прежде всего стоит разделять архитектурный и кодовый долг.
Архитектурный долг возникает тогда, когда структура системы перестаёт соответствовать текущим требованиям к развитию продукта. Сервисы начинают зависеть друг от друга сильнее, чем предполагалось изначально, появляются неожиданные точки связанности, изменения распространяются далеко за пределы одного модуля. Именно этот вид долга чаще всего определяет стоимость будущих изменений.
Кодовый долг проявляется иначе. Он связан с локальными инженерными решениями: дублированием логики, высокой цикломатической сложностью (cyclomatic complexity — показатель количества независимых путей выполнения кода), нарушением принципов проектирования, чрезмерной связанностью компонентов. Такой долг обычно легче локализовать и постепенно устранять, хотя полностью отделить его от архитектурного практически невозможно.
Отдельного разговора заслуживает тестовый долг, которому традиционно уделяют значительно меньше внимания.
Очень часто под ним понимают отсутствие автоматизации или недостаточное покрытие тестами. На мой взгляд, это слишком узкое определение.
Тестовый долг начинается в тот момент, когда сама тестовая система перестаёт развиваться вместе с продуктом. Появляются flaky-тесты, которые иногда проходят, а иногда падают без изменения кода. Один и тот же сценарий оказывается реализован несколькими способами. Подготовка тестовых данных занимает больше времени, чем сама проверка. Любое изменение функциональности требует корректировки десятков автотестов, хотя бизнес-логика практически не изменилась.
В этот момент тестовый набор превращается из инструмента ускорения разработки в самостоятельный объект сопровождения.
Именно тогда команда неожиданно обнаруживает, что значительная часть усилий уходит уже не на проверку продукта, а на обслуживание собственных тестов.
Почему тестирование принципиально не может уменьшить технический долг
Пожалуй, именно здесь проходит граница между тем, что обычно говорят о тестировании, и тем, как оно на самом деле работает в зрелых инженерных командах.
Мне кажется, одна из главных причин постоянных споров вокруг технического долга заключается в том, что мы смешиваем два совершенно разных процесса. Первый связан с тем, как система устроена, второй — с тем, как мы узнаём о её состоянии. Пока эти процессы не разделены, QA неизбежно начинают воспринимать как инструмент, который должен решить проблему накопленной сложности.
На практике это ожидание оказывается невыполнимым.
Архитектура определяет поведение системы задолго до того, как появляется первый тест. Именно на этапе проектирования принимаются решения о границах сервисов, степени связанности компонентов, распределении ответственности между модулями, модели данных и способах взаимодействия между подсистемами. Когда эти решения закрепляются в кодовой базе, система приобретает вполне определённые свойства. Одни изменения становятся локальными и безопасными, другие начинают затрагивать десятки компонентов одновременно.
Тестирование появляется значительно позже.
Оно работает уже не с архитектурными решениями, а с их последствиями.
Именно поэтому иногда приходится слышать фразу: «Мы увеличили покрытие, но разрабатывать быстрее не стало». На самом деле ничего удивительного здесь нет. Покрытие тестами действительно может повысить уверенность в изменениях, уменьшить количество дефектов, сократить вероятность регрессии. Но оно никак не влияет на то, насколько сложно внести само изменение.
Представьте вполне типичную для enterprise-проекта ситуацию. Изменение модели пользователя затрагивает сервис авторизации, профиль клиента, биллинг, Kafka-консьюмер обработки заказов, механизм уведомлений и несколько фоновых процессов, синхронизирующих данные с внешними системами. Формально разработчик меняет один объект. Фактически же команда вынуждена проверить десятки сценариев, потому что никто уже не может с уверенностью сказать, какие зависимости окажутся задействованы во время выполнения запроса.
В этом, на мой взгляд, и заключается принципиальное ограничение тестирования.
Оно не изменяет свойства инженерной системы.
Оно позволяет их наблюдать.
Мне нравится сравнение с медицинской диагностикой, хотя любые аналогии неизбежно упрощают реальность. Когда врач назначает пациенту компьютерную томографию, он не рассчитывает, что снимок исправит патологию. Ценность диагностики заключается совсем в другом: она позволяет увидеть изменения раньше, чем они станут необратимыми.
С инженерными системами происходит почти то же самое.
Когда в проекте начинают регулярно появляться flaky-тесты, увеличивается продолжительность регрессии, а разработчики всё чаще говорят, что перед изменением «нужно сначала разобраться, что ещё может сломаться», тестирование не создаёт эти проблемы. Оно лишь становится первой дисциплиной, которая начинает их фиксировать.
Именно поэтому зрелый QA значительно ближе к наблюдаемости (observability — наблюдаемость), чем к классическому контролю качества.
Хороший инженер по тестированию редко ограничивается ответом на вопрос «работает или не работает». Гораздо интереснее понять, почему каждое следующее изменение требует всё больше усилий, какие части системы становятся наиболее рискованными и в какой момент стоимость проверки начинает расти быстрее, чем объём новой функциональности.
Все эти признаки говорят не столько о качестве тестирования, сколько о состоянии самой инженерной системы.
Именно поэтому мне кажется важным изменить привычную постановку вопроса. Вместо того чтобы спрашивать, уменьшает ли QA технический долг, полезнее спросить, какие характеристики системы позволяют заметить, что этот долг начал влиять на скорость разработки.
Ответ на этот вопрос уже гораздо интереснее.
Что QA действительно способен измерить
Если посмотреть на ежедневную работу инженера по тестированию, можно заметить одну любопытную особенность. Значительная часть информации, которую команда собирает во время разработки, почти никогда не воспринимается как источник данных о техническом долге. Обычно её используют для оценки качества релиза, стабильности сборки или эффективности процесса. Между тем именно эти наблюдения зачастую позволяют раньше остальных заметить, что система постепенно начинает терять управляемость.
Исторически главной метрикой QA считалось количество найденных дефектов. Это вполне объяснимо: баг легко обнаружить, зарегистрировать в системе отслеживания задач, включить в отчёт и сравнить с предыдущим релизом. Однако с инженерной точки зрения эта метрика говорит удивительно мало. Она фиксирует уже произошедшее событие, но почти ничего не сообщает о том, почему вероятность подобных событий начала увеличиваться.
Дефект — это следствие. Технический долг всегда развивается раньше.
Именно поэтому в зрелых командах постепенно смещается сам фокус наблюдения. Инженеры начинают интересоваться не столько количеством ошибок, сколько тем, как меняется поведение системы во время разработки. Не случайно многие современные инженерные практики рассматривают качество не как характеристику релиза, а как характеристику процесса изменения программного продукта.
Одним из первых сигналов обычно становится Regression Cost — стоимость регрессионной проверки.
На практике Regression Cost редко сводится только ко времени выполнения тестов. Обычно он складывается из нескольких компонентов: подготовки тестовых данных, выполнения автоматизированных и ручных проверок, анализа результатов, повторных прогонов после исправлений и ожидания тестовых окружений. Именно поэтому две команды с одинаковым количеством тестов могут иметь совершенно разную стоимость регрессии — различаться будет не объём проверок, а цена их сопровождения.
Чаще всего её воспринимают как организационный показатель: сколько времени потребуется, чтобы убедиться, что после очередного изменения ничего не сломалось. Однако если отслеживать эту величину на протяжении нескольких месяцев, она начинает рассказывать гораздо более интересную историю.
В здоровой системе объём регрессионной проверки действительно растёт вместе с функциональностью продукта, но этот рост остаётся относительно предсказуемым. Когда же архитектура постепенно усложняется, картина меняется. Незначительное изменение неожиданно требует проверки десятков сценариев, подготовка тестовых данных занимает больше времени, чем сама проверка, а инженеры всё чаще добавляют ручные сценарии просто потому, что перестают доверять существующей автоматизации.
В инженерных командах это обычно ощущается довольно рано. После оценки новой задачи разговор неожиданно смещается с реализации на обсуждение последствий.
— «А это точно не затронет расчёт скидок?»
— «Нужно проверить ещё мобильное приложение.»
— «Не забудьте про отчёты — они используют этот сервис.»
Сам код ещё никто не начал писать, а стоимость изменения уже заметно выросла.
Похожим образом ведёт себя и Flaky Test Rate — доля нестабильных тестов.
Практически в каждой команде есть несколько тестов, которые иногда проходят успешно, а иногда завершаются ошибкой без каких-либо изменений в коде. Пока таких сценариев единицы, это воспринимается как обычная техническая проблема. Их переписывают, временно отключают или откладывают на потом. Но если количество flaky-тестов начинает стабильно увеличиваться, стоит задуматься не только о состоянии тестового набора.
Очень часто это говорит о том, что сама система становится менее детерминированной. Поведение компонентов начинает зависеть от состояния окружения, времени выполнения, порядка вызова сервисов или большого количества скрытых зависимостей. Другими словами, нестабильность тестов оказывается лишь отражением нестабильности архитектуры.
Причины при этом далеко не всегда находятся в тестовом коде. Рост Flaky Test Rate часто сопровождает появление гонок потоков (race conditions), скрытых зависимостей между тестами, асинхронных операций, нестабильных внешних сервисов или чувствительности системы к порядку выполнения сценариев. Поэтому опытные команды обычно начинают анализ не с переписывания тестов, а с поиска причин нестабильности самого приложения.
Кстати, сами flaky-тесты редко бывают одинаковыми. Чаще всего команда сталкивается с одной из нескольких типичных причин:
● зависимость результата от времени выполнения;
● влияние состояния тестового окружения;
● гонки потоков (race conditions — состязания потоков);
● нарушение порядка выполнения тестов;
● асинхронные операции, не завершившиеся к моменту проверки.
Именно поэтому массовое появление flaky-тестов обычно заставляет искать проблему значительно глубже, чем в самом тестовом коде.
Ещё одна метрика, которую, на мой взгляд, незаслуженно редко обсуждают, — Test Maintainability, или сопровождаемость тестового набора.
Любая автоматизация со временем начинает стареть. Это естественный процесс. Но существует важное различие между ситуацией, когда тесты приходится обновлять вслед за развитием продукта, и ситуацией, когда поддержка тестов начинает занимать больше времени, чем создание новых проверок.
Обычно ухудшение сопровождаемости становится заметно раньше, чем это начинает влиять на сроки релизов. Один Page Object внезапно начинает обслуживать несколько экранов, изменение одного API требует обновить десятки тестов, общие fixture (фикстуры — заранее подготовленные данные и объекты для тестов) превращаются в сложную сеть зависимостей, а время на поддержку тестового набора постепенно начинает превышать время написания новых проверок. Все эти признаки говорят о том, что сложность продукта уже начинает повторяться в самой автоматизации.
Во втором случае проблема почти никогда не ограничивается самим тестовым кодом. Как правило, тестовая система лишь повторяет структуру основного продукта. Если изменение одного пользовательского сценария требует переписать десятки автотестов, это означает, что сложность постепенно накапливается сразу в нескольких слоях инженерной системы.
Иногда ухудшение сопровождаемости тестов становится заметно ещё до того, как появляются серьёзные архитектурные проблемы. Например, изменение одного REST endpoint неожиданно требует обновить десятки Page Object, фабрик тестовых данных, моков внешних сервисов и общих фикстур. Формально продукт изменился совсем немного. Но стоимость сопровождения тестовой системы выросла непропорционально сильнее. Как правило, это означает, что тесты начали повторять недостатки архитектуры приложения.
Похожую роль играет и Change Failure Rate — доля изменений, которые приводят к инцидентам после поставки. Эта метрика получила широкую известность благодаря исследованиям DORA (DevOps Research and Assessment), однако её ценность выходит далеко за рамки оценки зрелости процессов разработки.
Когда Change Failure Rate начинает увеличиваться, команда обычно ищет причину в недостаточном тестировании или ошибках разработчиков. Иногда так действительно бывает. Но если показатель растёт на протяжении нескольких релизов подряд, значительно интереснее задать другой вопрос: не изменилась ли сама стоимость безопасного изменения системы?
Именно здесь технический долг впервые начинает проявляться не как абстрактное понятие, а как наблюдаемое инженерное явление.
По той же причине я всегда с осторожностью отношусь к обсуждениям покрытия кода тестами.
Покрытие остаётся полезной инженерной характеристикой, если понимать её ограничения. Оно позволяет оценить, какие участки системы вообще участвуют в автоматизированной проверке, но практически ничего не говорит о сложности самой системы. Два проекта могут иметь одинаковое покрытие, одинаковое количество автотестов и даже сопоставимый уровень качества релизов, однако стоимость изменения в одном из них окажется в несколько раз выше.
Само по себе покрытие не показывает, насколько безопасно изменять продукт.
Оно лишь показывает, какие части продукта проверяются автоматически.
Именно поэтому, когда речь заходит о техническом долге, мне кажется более важным наблюдать не за количеством тестов, а за тем, как постепенно меняются свойства инженерной системы. Если стоимость регрессии растёт быстрее продукта, сопровождаемость тестов ухудшается, а изменение небольшой функциональности требует всё более сложной проверки, проблема почти наверняка находится значительно глубже, чем кажется на первый взгляд
Какие метрики действительно помогают увидеть технический долг
В инженерной среде существует давняя привычка искать универсальную метрику качества. В разные годы эту роль пытались играть покрытие кода тестами, количество дефектов, процент успешных сборок, число автоматизированных сценариев и многие другие показатели. Каждый из них действительно помогает оценить отдельную сторону процесса разработки. Проблема возникает тогда, когда одну метрику начинают воспринимать как характеристику всей инженерной системы.
С техническим долгом такой подход практически никогда не работает.
Причина довольно проста. Сам долг невозможно измерить напрямую. У него нет собственного численного значения, которое можно вывести на дашборд (dashboard — информационная панель) или сравнить с предыдущим релизом. Мы всегда наблюдаем только последствия его накопления. Поэтому вопрос обычно звучит не «какая метрика показывает технический долг», а «какие изменения в поведении системы начинают говорить о том, что её сложность растёт быстрее, чем способность команды этой сложностью управлять».
Именно поэтому я постепенно перестал смотреть на отдельные показатели изолированно. Гораздо интереснее наблюдать, как они начинают влиять друг на друга.
Возьмём, например, покрытие тестами.
Наверное, это одна из самых популярных инженерных метрик последних двадцати лет. Она действительно полезна, когда нужно понять, какие части системы вообще остаются без автоматизированной проверки. Однако на этом её возможности практически заканчиваются.
Представим два проекта. В каждом из них покрытие составляет около восьмидесяти процентов. Если ориентироваться только на этот показатель, можно сделать вывод, что качество инженерных практик находится примерно на одном уровне. Но стоит посмотреть чуть глубже, как картина начинает меняться.
В первом проекте регрессия занимает меньше часа, тесты редко требуют сопровождения, а изменение пользовательского сценария приводит к корректировке двух-трёх проверок.
Во втором проекте те же восемьдесят процентов покрытия достигаются ценой огромного набора тесно связанных автотестов, которые приходится переписывать практически после каждого заметного изменения интерфейса или бизнес-логики.
Формально метрика одинакова.
Инженерное состояние системы — совершенно разное.
Именно поэтому покрытие тестами почти ничего не говорит о техническом долге.
Гораздо интереснее становится Regression Cost.
Если стоимость полной проверки продукта начинает расти быстрее самого продукта, это почти всегда означает, что сложность системы увеличивается. Причём причина может скрываться где угодно: в архитектуре, тестовой инфраструктуре, накопленных интеграциях или изменении процесса поставки. Для QA это не так важно. Важно другое — команда начинает тратить всё больше усилий только для того, чтобы сохранить прежний уровень уверенности перед релизом.
Похожим образом работает Change Failure Rate.
Эта метрика широко известна благодаря исследованиям DORA, опубликованных в книге Accelerate и чаще используется для оценки зрелости процессов разработки. Авторы показали, что способность команды безопасно поставлять изменения лучше характеризует зрелость инженерной организации, чем многие традиционные показатели производительности. Тем не менее она довольно точно отражает ещё одну важную особенность системы. Когда доля изменений, приводящих к инцидентам, постепенно увеличивается, это редко бывает следствием одной ошибки разработчика или одного неудачного релиза. Гораздо чаще причина оказывается системной. Изменения становятся менее предсказуемыми, а последствия — более трудно прогнозируемыми. Именно так обычно начинает проявляться накопленная сложность.
Не менее показательной оказывается сопровождаемость тестового набора.
На мой взгляд, команды незаслуженно редко обсуждают стоимость сопровождения автоматизации как самостоятельную инженерную характеристику. Между тем именно она часто становится одним из первых индикаторов деградации системы.
Когда поддержка тестов начинает занимать столько же времени, сколько разработка новых сценариев, проблема почти никогда не ограничивается самим тестовым кодом. Как правило, тестовая система лишь повторяет структуру основного продукта. Если она становится трудно сопровождаемой, имеет смысл внимательно посмотреть и на архитектуру приложения.
Особое место я бы отвёл Flaky Test Rate.
Обычно flaky-тесты воспринимаются как локальная проблема инфраструктуры или неудачно написанной автоматизации. Иногда так и есть. Но если их количество стабильно растёт, я бы не стал начинать анализ с переписывания тестов.
Сначала я попробовал бы ответить на другой вопрос.
Почему система стала настолько чувствительной к малейшим изменениям окружения?
Почему результаты выполнения всё чаще зависят от времени, порядка запуска сервисов или состояния внешних компонентов?
Очень часто выясняется, что нестабильность тестов оказывается не причиной, а следствием архитектурной нестабильности.
Есть ещё одна метрика, которая практически не встречается в отчётах QA, хотя лично мне она кажется одной из самых полезных.
Это стоимость анализа изменения.
Её нельзя получить автоматически. Её не покажет SonarQube и не построит Grafana. Но её довольно хорошо чувствует любая зрелая команда.
Сколько времени проходит между моментом, когда разработчик получает задачу, и моментом, когда он действительно начинает писать код?
Если большая часть этого времени уходит на изучение зависимостей, поиск возможных побочных эффектов, консультации с другими командами и попытки понять, что именно может сломаться после изменения, то технический долг уже начал влиять на инженерную производительность, даже если остальные метрики пока выглядят вполне благополучно.
Именно поэтому мне кажется опасной сама идея поиска одной «главной» метрики технического долга.
Её не существует.
Технический долг — это изменение поведения инженерной системы. А любое сложное поведение почти никогда не описывается одним числом.
Гораздо полезнее наблюдать за тем, как постепенно меняется сразу несколько характеристик. Если одновременно увеличивается стоимость регрессии, ухудшается сопровождаемость тестов, растёт Change Failure Rate и команде требуется всё больше времени на анализ изменений, то почти наверняка мы имеем дело не с локальной проблемой процесса, а с накоплением системной сложности.
Именно эта совокупность признаков, на мой взгляд, позволяет увидеть технический долг значительно раньше, чем он начинает проявляться в виде срывов сроков, аварийных релизов или дорогостоящих архитектурных переписываний.
|
Метрика |
Что показывает |
Где полезна |
Чего не показывает |
|
Defect Density |
Качество реализации конкретной функциональности |
Контроль релизов |
Сложность инженерной системы |
|
Test Coverage |
Область автоматизированной проверки |
Анализ пробелов тестирования |
Архитектурный долг |
|
Regression Cost |
Стоимость проверки изменений |
Раннее обнаружение роста сложности |
Причину роста стоимости |
|
Flaky Test Rate |
Стабильность тестовой среды и взаимодействий |
Поиск системной нестабильности |
Конкретный источник проблемы |
|
Test Maintainability |
Стоимость сопровождения автоматизации |
Оценка тестового долга |
Качество бизнес-логики |
|
Change Failure Rate |
Предсказуемость изменений |
Оценка зрелости инженерной системы |
Архитектурную причину отказов |
|
|
|
|
|
Любопытно, что почти все эти метрики являются запаздывающими индикаторами. Они не показывают момент появления технического долга, а лишь фиксируют изменение свойств системы после того, как архитектурные решения уже были приняты. Именно поэтому их имеет смысл рассматривать не по отдельности, а в динамике нескольких последовательных релизов.
Как встроить контроль технического долга в SDLC
Когда разговор заходит о техническом долге, команда почти всегда начинает обсуждать способы его устранения. Планируются отдельные спринты на рефакторинг, составляются списки проблемных модулей, появляются задачи на переписывание отдельных компонентов. Всё это может быть полезно, но есть одна проблема.
К тому моменту, когда возникает необходимость выделять отдельное время на борьбу с техническим долгом, система уже успела существенно измениться.
На практике гораздо полезнее не устранять долг постфактум, а сделать его накопление наблюдаемым в процессе разработки. Для этого не нужен отдельный процесс. Достаточно встроить несколько инженерных практик в обычный SDLC (Software Development Life Cycle — жизненный цикл разработки программного обеспечения).
Оценивать не только стоимость разработки, но и стоимость изменения
Во многих командах оценка новой задачи заканчивается после обсуждения реализации. Разработчики определяют объём изменений, архитектор оценивает влияние на систему, после чего задача попадает в спринт.
При этом почти никогда не обсуждается другая величина — сколько будет стоить безопасно доставить это изменение до production (рабочей среды).
Если изменение одной пользовательской функции требует перепроверить пять сервисов, три интеграции и несколько десятков существующих сценариев, это уже инженерный сигнал. Возможно, сама задача небольшая, но стоимость её безопасной поставки начинает расти.
Поэтому во время планирования полезно задавать ещё один вопрос:
Как изменится стоимость проверки после реализации этой задачи?
Если практически каждое изменение увеличивает объём обязательной регрессии, количество ручных проверок или время анализа последствий, технический долг уже влияет на процесс разработки.
Наблюдать за состоянием тестовой системы так же внимательно, как за состоянием продукта
Большинство инженерных метрик относятся к самому приложению. Мы измеряем количество дефектов, стабильность сборок, производительность сервисов и скорость поставки изменений.
Тестовая система при этом нередко остаётся без собственного набора характеристик, хотя она развивается одновременно с продуктом и со временем начинает наследовать его сложность.
На практике достаточно регулярно отслеживать несколько показателей:
● количество flaky-тестов и скорость их устранения;
● время выполнения полного regression suite;
● долю тестов, изменённых в каждом релизе;
● среднее время сопровождения автоматизации;
● количество отключённых (quarantined) тестов;
● долю ручных проверок, появившихся из-за недостатка доверия к автоматизации.
По отдельности эти показатели мало о чём говорят. Но если они начинают ухудшаться одновременно, обычно это означает, что сложность продукта постепенно переносится и в тестовую систему.
Анализировать не только код, но и стоимость будущих изменений
Code review давно стал обязательной частью разработки. Значительно реже команды анализируют, как конкретное изменение повлияет на дальнейшее развитие системы.
Между тем именно такой разговор помогает раньше остальных заметить накопление технического долга.
Для крупных изменений обычно достаточно ответить всего на несколько вопросов.
● Появились ли новые зависимости между сервисами?
● Увеличился ли объём обязательной регрессии?
● Потребовались ли дополнительные ручные проверки?
● Стало ли сложнее локализовать влияние будущих изменений?
● Возникли ли новые общие компоненты, от которых теперь зависит несколько модулей?
Если положительных ответов становится всё больше, система постепенно теряет способность к локальным изменениям.
Использовать метрики как повод для исследования, а не как KPI
Пожалуй, именно здесь команды чаще всего совершают одну и ту же ошибку.
Как только инженерная метрика становится KPI, люди начинают оптимизировать не инженерную систему, а сам показатель. Растёт покрытие тестами, уменьшается количество зарегистрированных дефектов, ускоряется выполнение отдельных сценариев — при этом стоимость изменений может продолжать увеличиваться.
Поэтому любые инженерные метрики полезно воспринимать не как итоговую оценку качества, а как сигнал к исследованию.
Рост Change Failure Rate — повод разобраться, почему изменения стали менее предсказуемыми.
Рост Regression Cost — повод посмотреть, какие архитектурные решения начинают влиять на стоимость проверки.
Рост Flaky Test Rate — повод исследовать взаимодействие компонентов, а не только тестовый код.
Хорошая инженерная метрика редко отвечает на вопрос сама по себе. Гораздо чаще она помогает понять, какой вопрос стоит задать команде.
Сделать наблюдение постоянной частью SDLC
Технический долг редко появляется после одного неудачного релиза. Гораздо чаще он накапливается настолько постепенно, что команда просто перестаёт замечать происходящие изменения. Регрессия становится длиннее на десять минут, анализ новой задачи требует ещё одного совещания, изменение одного сервиса начинает затрагивать четыре соседних вместо трёх. По отдельности такие отклонения почти незаметны.
Опасность возникает тогда, когда они становятся новой нормой.
Именно поэтому наблюдение за техническим долгом не должно превращаться в отдельную инициативу, которую вспоминают раз в полгода. Оно должно происходить постоянно — вместе с code review, анализом инцидентов, ретроспективами и планированием изменений.
В конечном счёте задача QA заключается не только в том, чтобы подтвердить корректность очередного релиза.
Гораздо важнее вовремя заметить момент, когда цена каждого следующего изменения начинает расти быстрее самого продукта.
Заключение
В инженерной среде довольно легко привыкнуть к мысли, что технический долг — это ещё одна задача в бэклоге. Его можно оценить, запланировать, постепенно уменьшить, а если повезёт — когда-нибудь полностью закрыть. Чем дольше работаешь с крупными системами, тем меньше в это верится.
Технический долг — не объект. Это состояние системы.
Он не появляется после одного неудачного релиза и не исчезает после одного удачного рефакторинга. Он постоянно меняется вместе с продуктом, потому что каждое архитектурное решение, каждое новое ограничение и каждая интеграция немного меняют стоимость следующих изменений.
Именно поэтому, на мой взгляд, ожидать, что QA сможет уменьшить технический долг, — значит требовать от тестирования того, для чего оно никогда не создавалось.
Тестирование не делает архитектуру проще.
Не уменьшает связанность между сервисами.
Не устраняет циклические зависимости.
Не снижает сложность предметной области.
Все эти свойства появляются значительно раньше первого теста.
Зато тестирование делает другую, не менее важную работу — оно позволяет увидеть, что инженерная система начала меняться. Причём зачастую раньше, чем это становится заметно разработчикам, архитекторам или владельцам продукта.
Рост стоимости регрессии, увеличение количества flaky-тестов, ухудшение сопровождаемости автоматизации, изменение Change Failure Rate, увеличение времени анализа влияния изменений — всё это редко возникает одновременно случайно. Как правило, подобные сигналы говорят о том, что система постепенно теряет способность безопасно изменяться.
Именно здесь проходит граница между поиском дефектов и инженерным наблюдением. Незрелая команда использует тестирование, чтобы понять, работает ли система сегодня.
Зрелая команда использует тестирование ещё и для того, чтобы понять, насколько дорого ей будет изменить эту систему через полгода.
Разница кажется незначительной только на первый взгляд. В первом случае QA отвечает за поиск дефектов. Во втором — становится одним из источников инженерной наблюдаемости (observability — наблюдаемость), позволяющим увидеть деградацию системы задолго до того, как она начнёт проявляться в авариях, сорванных сроках или бесконечных обсуждениях очередного «большого рефакторинга».
И, пожалуй, именно это изменение взгляда кажется мне самым важным.
Мы привыкли оценивать качество по тому, насколько успешно прошёл очередной релиз. Возможно, пришло время задавать другой вопрос.
Не «насколько хорошо работает система сегодня», а «сохранила ли она способность изменяться завтра».
ссылка на оригинал статьи https://habr.com/ru/articles/1055226/