Меня зовут Даниил, я Android-разработчик в «БКС Мир инвестиций».

В первой статье мой коллега рассказывал, как мы использовали Kotlin IR Compiler Plugin, чтобы автоматически добавлять testTag и semantics в Compose-компоненты: Kotlin IR Compiler Plugin в дизайн-системе: автотесты с Compose без ручной разметки.

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

Если коротко, то кейс выглядит вот так:

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

  • Решение: мы строим карту влияния — структурированное описание компонентов и связей «кто кого использует», на уровне самой дизайн-системы. Compiler plugin сохраняет manifest этих связей, а impact analysis превращает изменение в описанную зону риска.

  • Результат: CI в первую очередь запускает проверки, привязанные к затронутым компонентам и к цепочке их использования от источника изменения, а отчёт в merge request показывает автору, ревьюеру, тестировщику и дизайнеру, что задели напрямую, что может быть затронуто по цепочке, какие проверки уже есть и где покрытие стоит усилить.

Для нас это оказалось важнее, чем просто “запускать меньше тестов”. Impact analysis стал общим языком между CI, разработчиком, ревьюером, тестировщиком и дизайнером. CI получает список необходимых проверок, а люди получают контекст риска: какие компоненты и сценарии стоит проверить дополнительно, даже если они не менялись напрямую.

Дальше расскажу, как мы переложили impact analysis на язык компонентной библиотеки: построили карту зависимостей, связали её с превью и тестами и превратили результат в понятный отчёт для merge request.

Почему полный прогон перестал быть хорошим ответом

Пока библиотека компонентов небольшая, полный прогон тестов после каждого изменения выглядит естественно. Он простой, понятный и безопасный.

Но дизайн-система постепенно растёт:

  • компонентов становится больше;

  • появляются разные уровни: core, base, составные компоненты;

  • один компонент начинает использовать другие;

  • у компонентов появляются состояния, варианты отображения, превью;

  • растёт число unit, screenshot, автотестов;

  • разные продуктовые команды зависят от одной общей библиотеки.

В такой системе маленькая правка не всегда остаётся маленькой.

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

У нас одновременно появилось несколько проблем:

  • поиск зависимых компонентов: нужно понимать, куда может дойти изменение;

  • скорость CI: полный прогон всех проверок становится дорогим;

  • хрупкость ручного выбора тестов: легко забыть проверить компонент, который использует измененный компонент;

  • неочевидные зоны, где покрытие стоит усилить: иногда изменение задевает компонент, у которого нет превью или screenshot-теста;

  • сложность review и ручной проверки: ревьюеру, тестировщику и дизайнеру нужно понять не только “что поменялось”, но и “что это может задеть”.

Так появилась задача: построить механизм, который показывает область влияния изменения, помогает CI выбрать набор проверок по графу зависимостей от источника изменения и даёт участникам MR понятный список компонентов и сценариев, на которые имеет смысл обратить внимание при приёмочной проверке или при ручном прогоне в демо.

Что обычно делают в таких случаях

Проблема выборочного запуска проверок не новая. Когда тестов становится много, команды обычно приходят к одному и тому же вопросу: как не запускать всё каждый раз, но всё ещё доверять CI?

Мы смотрели на несколько подходов.

Первый подход — полный регресс. Его плюс очевиден: он даёт максимум уверенности. Минус тоже очевиден: чем больше проект, тем дороже каждый прогон. Из этого подхода мы забрали важный принцип: полный прогон всё равно должен остаться как безопасный fallback.

Второй подход — smoke/sanity-наборы. Они хорошо отвечают на вопрос “система вообще жива?”. Но нам нужен был другой ответ: “что именно могло сломаться из-за этого изменения?”. Поэтому smoke-наборы полезны как быстрый сигнал, но не решают задачу анализа влияния.

Третийподход — impact analysis на уровне проекта или модулей. Такой подход хорошо описан в Android-сообществе. Например, Циан писал, как ускорять проверки через impact-анализ для статических анализаторов и unit-тестов. Ситимобил показывал impact-анализ на примере Android-проекта.

Из этих материалов для нас были важны две мысли:

  • анализ должен идти не только от изменённого места, но и по зависимостям;

  • результат анализа должен быть пригоден и для CI, и для людей, которые принимают решение в MR.

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

Поэтому мы взяли общую идею impact analysis, но применили её к компонентам дизайн-системы: их связям, состояниям, превью и тестовому покрытию. Границы Gradle-модулей и экранов приложения здесь вторичны; важнее вопрос «какой компонент затронут, кто его использует, какие превью и тесты относятся к этой части графа?» — именно так мы переложили impact analysis на язык компонентной библиотеки.

Представить дизайн-систему как карту влияния

Дальше под картой мы будем иметь в виду описание компонентов и рёбер «использует / используется в» между ними; позже к этой базе добавятся state, превью и привязки тестов.

Мы начали с простого вопроса:

Если изменился компонент, как понять, какие компоненты могут быть затронуты этим изменением?

Ответ оказался в структуре самой дизайн-системы.

Компоненты связаны между собой:

  • один компонент может использовать другой;

  • базовый компонент может входить в несколько составных;

  • состояние одного уровня может влиять на поведение другого;

  • тест может проверять не один компонент, а связку из нескольких.

Например, изменение в ButtonCore может быть важно для ButtonBase, а через него - для ButtonBarBase.

Примерная иерархия компонента
Примерная иерархия компонента

Картинка 1. Цепочка ButtonCoreButtonBaseButtonBarBase: изменение в нижнем слое может подняться выше по использованию (выше по схеме — «…», иерархия продолжается).

Так обычная иерархия UI-компонентов превращается в граф влияния: мы видим не только место изменения, но и путь, по которому оно может подняться выше.

Откуда берётся карта

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

У нас уже был более точный источник данных - compiler plugin из первой статьи. Если совсем кратко, plugin проходит по IR-дереву, находит компоненты с @CodegenModifier и работает не с текстом файла, а со структурой Kotlin-кода. Подробно этот механизм разобран отдельно: Kotlin IR Compiler Plugin в дизайн-системе: автотесты с Compose без ручной разметки.

Для impact analysis мы используем тот же этап компиляции, но сохраняем не test semantics, а карту компонентов в manifest. По сути, manifest - это JSON-слепок структуры дизайн-системы: имя компонента, путь к исходнику, слой, прямые зависимости и связанные state-модели.

Например, одна запись в manifest может выглядеть так:

{
  "componentName": "ButtonCore",
  "sourcePath": "compose_uikit/.../button/ButtonCore.kt",
  "layer": "core",
  "directComponentDependencies": [
    "Typography"
  ],
  "stateReferences": [
    {
      "typeName": "ButtonCoreState",
      "sourcePath": "compose_uikit/.../button/ButtonCoreState.kt"
    }
  ]
}

Сам процесс генерации устроен так:

  1. Kotlin-компилятор строит IR-дерево проекта.

  2. Наш compiler plugin проходит по этому IR-дереву.

Почему опираемся на IR, а не переключаемся, скажем, на FIR или разбор исходников как AST? На другой стадии компилятора тоже можно было бы извлечь зависимости, но у нас уже есть рабочий IR-проход из первой статьи: один pipeline, одна точка сопровождения, меньше дублирования. IR в нашем случае — зрелое представление программы, по которому стабильно обходить вызовы и структуру кода; второй плагин «рядом» оказывается дешевле, чем параллельная реализация на другом уровне ради того же результата.

  1. Когда plugin находит функцию с @CodegenModifier, он собирает данные о компоненте: имя, путь к исходнику, слой, прямые зависимости и связанные state-модели.

  2. Эти данные складываются во внутренний список записей manifest.

  3. После обхода модуля plugin записывает список в JSON-файл.

То есть manifest не поддерживается руками. Он появляется автоматически во время компиляции и становится готовой картой компонентов для дальнейшего анализа.

Общая схема impact analysis
Общая схема impact analysis

Картинка 2. Общая схема: данные о компонентах собираются заранее, а затем используются для анализа изменений и выбора проверок.

Как определяется зона риска

Зона риска - это ответ на вопрос: “куда стоит посмотреть после изменения?”.

Допустим, изменился ButtonCore. Это не значит, что проблема может быть только внутри ButtonCore. Он может быть маленькой деталью, на которой держатся более крупные компоненты.

Анализ начинает с источника изменения и постепенно расширяет область:

  1. Сначала в зоне риска сам ButtonCore.

  2. Потом компоненты, которые используют его напрямую, например ButtonBase.

  3. Затем компоненты выше по цепочке, например ButtonBarBase.

В отчёте такая цепочка может выглядеть так:

ButtonCore -> ButtonBase -> ButtonBarBase

Важно: это не “порядок выполнения кода”, а маршрут распространения риска. Если мы меняем нижний слой (ButtonCore), проблема может проявиться не сразу в нём, а выше — там, где этот слой используется как часть более сложного компонента. Поэтому в review полезно смотреть не только дифф исходного файла, но и всю цепочку использования.

Если изменение касается общей модели состояния, зона риска может расшириться сразу в несколько веток. Это не ошибка анализа, а полезный сигнал: общий state действительно может влиять на несколько компонентов одновременно.

Дальше к этой зоне добавляются практические подсказки:

  • в каком демо быстрее всего проверить сценарий вручную;

  • какие тесты уже покрывают эту область;

  • где покрытие стоит усилить (например, у затронутого компонента нет screenshot-теста или собственного превью);

  • какие проверки имеет смысл запустить первыми.

Как работает impact analysis
Как работает impact analysis

Картинка 3. На схеме подсвечены ключевые блоки: источник изменения, распространение по цепочке компонентов, покрытие тестами, зоны, где покрытие стоит усилить, и приоритет проверок — в том же порядке, что и практические подсказки в тексте выше.

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

Как тесты связываются с компонентами

Карта компонентов показывает зону риска, но сама по себе ещё не говорит, какие тесты нужно запускать. Чтобы замкнуть цепочку, мы добавили второй тип данных: точки покрытия тестов.

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

Граф зависимостей между компонентами отвечает на вопрос: куда может распространиться изменение. Compiler plugin при компиляции строит manifest: какой composable является компонентом, какие state-модели он использует и какие другие компоненты вызывает.

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

Например, unit-тест формата состояния кнопки привязывается к ButtonBase:

@TestedComponent("ButtonBase")
class ButtonBaseTest {
    // unit tests
}

Если потом меняется ButtonCore, impact analysis проходит по графу вверх, видит, что изменение влияет на ButtonBase, и выбирает тесты, привязанные к ButtonBase.

изменение: ButtonCore
граф:      ButtonCore -> ButtonBase
тесты:     ButtonBaseTest, ButtonBasePaparazziTest

В этом и есть важная особенность схемы: тест описывает свой проверяемый контракт, а impact analysis сам сопоставляет этот контракт с зоной риска.

Один тест может иметь и несколько точек покрытия. Это полезно для сценариев, которые проверяют общий контракт нескольких самостоятельных компонентов. Например, unit-тест может проверять общий formatter, используемый в InputValueBase и InputDateBase, или screenshot-сценарий может сравнивать несколько публичных вариантов компонента в одном тестовом классе:

@TestedComponent("InputValueBase", "InputDateBase")
class InputFormattingTest {
    // tests for shared visible formatting contract
}

Если завтра InputDateBase начнёт использовать новый внутренний InputCalendarCore, граф увидит новую связь сам, а тест останется привязан к публичному компоненту, поведение которого он проверяет. Так покрытие остаётся привязанным к продуктовой сути сценария, а не к текущей внутренней реализации.

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

Что видит команда в MR

Главный результат для команды - понятный отчёт в merge request.

Упрощённо отчёт может выглядеть так:

Источник изменения:
- ButtonCoreState

Зона риска:
- ButtonCore
- ButtonBase
- ButtonBarBase

Цепочки влияния:
- ButtonCore -> ButtonBase
- ButtonCore -> ButtonBase -> ButtonBarBase

Найденные проверки:
- ButtonCoreStateTest
- ButtonBaseTest
- ButtonBasePaparazziTest

Зоны, где покрытие стоит усилить:
- ButtonBarBase без собственного превью
- ButtonBarBase без screenshot-теста

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

Что меняется в CI

В CI impact analysis работает как фильтр перед запуском проверок.

Если зона риска понятна, CI начинает с тестов, которые связаны именно с ней. Например, если изменение вокруг кнопки, сначала запускаются проверки кнопки и компонентов, которые её используют.

Работа CI
Работа CI

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

В типовом случае это сокращает объём проверок и ускоряет обратную связь в merge request.

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

Практический эффект для команды

Чтобы не оставаться на уровне “стало удобнее”, мы смотрели на три прикладных эффекта: время до первого полезного сигнала в MR, долю полных прогонов и понятность review.

  • Скорость обратной связи. В типичных MR с локальными правками first feedback приходит быстрее, потому что CI начинает с узкого набора проверок по графу затронутых компонентов, а не с полного suite.

  • Изменение workflow. Автор MR сначала читает impact-отчёт, проверяет риск-цепочки и запускает релевантные тесты адресно. Ревьюер, тестировщик и дизайнер получают ту же карту и быстрее выбирают, куда смотреть глубже.

  • Окупаемость. Подход особенно окупается в потоках с частыми изменениями базовых компонентов: меньше “лишних” прогонов, меньше ручного гадания по зависимостям, меньше возвратов в MR после поздно найденных эффектов.

Мы сознательно оставили полный регресс как страховку. Поэтому выигрыш здесь не в “максимальном урезании тестов”, а в том, что большая часть обычных MR обрабатывается быстрее и предсказуемее без потери безопасного сценария.

Что изменилось в процессе

Главное изменение оказалось не только в скорости CI. До impact analysis проверка выглядела как чёрный ящик: разработчик отправил MR, CI что-то запустил, в конце сталопонятно — зелёное или красное.

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

Это полезно и для автора MR, и для ревьюера, и для тех, кто проверяет поведение или визуальное состояние компонента руками. Изменение перестаёт быть локальной догадкой и становится описанной областью риска для всех участников процесса.

Где подход может подвести

Impact analysis настолько хорош, насколько хороши данные, на которых он построен.

Есть два неприятных сценария.

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

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

Поэтому мы не считаем результат анализа абсолютной истиной. Это подсказка, а не оракул.

Качество данных приходится поддерживать отдельно:

  • компоненты должны попадать в карту через общий механизм разметки;

  • тесты должны явно говорить, какие компоненты они покрывают;

  • полный прогон должен оставаться fallback для сложных или сомнительных случаев.

Отдельный компромисс — annotation-based mapping тестов. Да, тест нужно явно связать с компонентом. Это дополнительная дисциплина. Но зато связь видна в коде, ревьюится вместе с тестом и не живёт в отдельной ручной таблице, которую легко забыть обновить.

В итоге impact analysis помогает сфокусироваться, но не заменяет review и инженерное мышление.

Что можно развивать дальше

Дальше хочется сделать анализ точнее: учитывать не только факт изменения state-модели, но и конкретные поля, которые использует компонент.

Ещё одно направление — связать impact analysis с тестовым контрактом из первой статьи. Если компонент уже описывает себя через стабильные test semantics, на этой базе можно строить Page Object или подсказки для тестировщиков.

Краткий итог

Наша исходная гипотеза была простой: если построить карту влияния изменения и связать её с покрытием, то CI станет быстрее и полезнее для принятия решений, а не только для “красный/зелёный”.

На практике это подтвердилось: impact analysis стал для нас способом сделать изменения в дизайн-системе ожидаемыми.

Не просто “поменяли файл - запустили тесты”, а:

изменение
  -> источник риска
  -> затронутые компоненты
  -> связанные проверки
  -> понятный feedback в MR

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

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

На примере ButtonCore это уже видно в минутах: в локальном замере targeted-прогон unit и Paparazzi занял 42 секунды вместо 53 секунд полного прогона. При этом ButtonCore — низкий компонент в иерархии, от которого расходится широкая зона влияния. Для более локальных изменений выигрыш будет заметнее, а по мере роста количества тестов вокруг компонентов разрыв между полным suite и targeted-прогоном будет только увеличиваться.

Когда такая модель появляется, CI перестаёт быть чёрным ящиком с кнопкой “запустить всё”. Он становится частью инженерной обратной связи: показывает impact, подсвечивает риски и помогает быстрее принимать решения в merge request.

Комментарии (1)


  1. ws233
    27.05.2026 08:49

    Отличная глубокая работа. Особенно понравился анализ эффектов, проблем, процессов и т.д. Все б так свою работу делали... А то большинство просто реализует бездумно...

    Несколько моментов, если позволите.

    Сейчас в недостатках у вас не указано, что необходимо писать "разметчик". Это трудозатратно, это человеческий фактор, про который Вы в принципе указали, отметив "второй неприятный сценарий". Более того, этот же недостаток будет далее влиять на ваши будущие планы. В частности, если хотите расширять на то, что проверяется, это опять вам придется расширять разметчик, а потом еще что-то захотите, а потом еще – и так никогда не остановитесь...

    Есть сильно более простой, более автоматизируемый и более гарантированный способ.

    Следите за руками.

    1. Тесты вам выдают "покрытие" - номера строк кода, по которым прошелся данный тест.

    2. Гитхаб вам выдает "изменения" - номера строк кода, которые изменились.

    3. Находите пересечение первого списка со вторым и получаете тесты, которые вам нужно запустить. Остальные можно смело не запускать. Почему? Да потому, что если тест не затронул ни одной измененной строки, то зачем его запускать? Результат его прогона не изменится никак.

    4. Вы великолепны!

    В этом подходе нет перечисленных Вами и мной недостатков, он не требует разметки (что вообще жесть, т.к.есть такое базовое правило тестирования: "боевой код не должен меняться в угоду тестам!", ибо это вообще жесть, мы можем внести ошибку в боевой код только из-за того, что мы хотим его протестировать – абсурд же, верно?). Ему можно доверять, нет "непроверенных" связей. Мы запускаем гарантированно те тесты, которые бегают по измененным строчкам, никаких лишних.

    Что скажете? Увидим мы новую статью в ближайшее время от Вас с этим подходом, или я, может, где-то ошибся, и Вы слету видите эту ошибку?

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