Detekt: как статический анализ помогает улучшить код автотестов |
13.02.2024 00:00 |
Есть такое мнение, что качество кода автотестов не так важно в сравнении с основной кодовой базой. Однако это тоже код, который приходится поддерживать с соответствующими накладными расходами. Если не следить за его качеством, то и тут могут возникать проблемы. И у каждой ошибки есть своя цена. Было бы здорово, если бы о них можно было узнать:
Это может касаться как простых ошибок, на которые не хочется тратить время специалистов, так и неочевидных ошибок, у которых иногда непросто определить причину. Меня зовут Николай, и я инженер в мобильной платформенной команде Яндекс Еды. В этой статье я расскажу, как мы повышаем качество кода автотестов Android-приложения. И в этом нам помогает статический анализ. Коробочные правилаПрежде всего, коротко расскажу про наш стек: это Kotlin, Kaspresso и Marathon. В качестве основного статического анализатора используем Detekt, который строится на паттерне Visitor и в процессе анализа исходного кода может инспектировать различные языковые конструкции Kotlin: например, функции, переменные и многие другие. На Хабре уже есть статьи про первичную настройку Detekt (ссылки на них дам в конце), поэтому опустим этот этап и перейдём к рассмотрению нашего опыта. Итак, в стандартной библиотеке Detekt много правил. Но я приведу те, которые, по моему мнению, могут быть полезны для кодовой базы автотестов:
Стандартные правила неплохи, но они не покрывали частые проблемы из нашей кодовой базы автотестов. У нас были целые спринты по их исправлению. Например, мы думали, как починить длительное выполнение части тестов с периодическими простоями или flaky. Так или иначе, мы смогли написать свои правила, которые помогли нашему коду стать лучше. Этому и будет посвящено моё дальнейшее повествование. Примеры наших правилУ вас, конечно, может возникнуть желание покрыть правилами всё и сразу, но более удачным решением будет составить список известных проблем. Вы сможете найти их в комментариях к Pull Request и прочих артефактах совместной работы. В первую очередь можно покрыть то, что соответствует критерию «быстро и просто», а редко встречающиеся проблемы можно отложить до лучших времён. В первой итерации такого подхода мы создали семь правил. IsVisibleUsageRuleПредположим, что мы хотим проверить, отображается ли кнопка. Первая мысль, которая приходит в голову, — использовать говорящую проверку Также правильный выбор проверки оказывает влияние на стабильность за счёт Kaspresso flakySafety. Ведь значение свойства
LargeScreenObjectRuleБольшие Page Object поддерживать достаточно сложно. Например, экран, который содержит много логики и элементов, либо несколько экранов, которые содержат очень похожий повторяющийся элемент. Либо когда в приложении есть feature toggle и у части элементов может быть несколько реализаций с разными идентификаторами. В таких случаях мы ограничиваем размер в пользу Page Element в составе Page Object. При этом можно не начинать с преждевременных оптимизаций, а переработать уже существующие большие Page Object. Авторы Kaspresso рекомендуют использовать не больше одной абстракции, но я разделяю несколько другую точку зрения и допускаю использование Page Element как более атомарной части экрана приложения. Вероятно, вы не сможете с наскока переписать все большие Page Object, и вам может потребоваться baseline.xml, чтобы временно игнорировать нарушения (о нём я расскажу немного позже). RestrictedKeywordRuleТесты — вещь однозначная, поэтому в них не должно быть ветвлений. Именно поэтому мы запретили конструкции
Также наличие конструкций А ещё неправильная обработка ошибок на уровне теста, Scenario и Page Object приводит к ожиданию до тайм‑аута (у нас это 20 секунд), поэтому теперь мы запрещаем TestClassNamingRuleМногим AQA известно, что название тестового класса в Junit4 должно содержать в конце
TestClassPrivateMemberRuleЕсли при создании теста вы копируете существующие фрагменты кода, то они могут начать ссылаться на константы тестов доноров не из Константы должны находиться внутри Вспомогательные функции и нелокальные переменные из тестовых классов тоже должны быть приватными ради единообразия, чтобы не спорить о мелочах на ревью.
TestMethodNamingRuleДлина квалифицированного пути тестового метода должна быть не больше зафиксированного значения. С таким квалифицированным именем будет удобно работать в marathon-файле в Название тестовой функции не должно содержать слово
TmsLinkAnnotationRuleЭто правило специфично для нашего CI, но у вас может быть похожая ситуация. Мы связываем тест-кейс и класс автотестов через аннотацию
Также мы проверяем строковое содержимое аннотации через RegExp, а ещё то, что не используется очень похожая От чего мы пока отказалисьУ нас было ещё несколько идей правил, которые не попали в первую итерацию. Но так или иначе они имеют право на существование. Возможно, эти мысли окажутся полезными для вашего проекта. Запрет в тесте неявных ожиданий. Если вы не дожидаетесь нужного события явно, а просто ждёте Х секунд через Запрет больших JSON-моков. Если JSON-мок большой, то это может создавать определённые сложности. Чем больше в моке не используемых автотестами сущностей (например, на витрине магазина), тем дольше приложение будет отрисовывать их и, следовательно, дольше будут идти и сами тесты. Также структура файла в IDE будет анализироваться дольше. Detekt — это анализатор кода на Kotlin. Проверка JSON или XML в нём не предусмотрена. Пока что отложили эту идею и, возможно, сделаем позже с помощью Android Lint. Запрет наследования неподходящих классов. При описании Kaspresso‑экранов возможно использовать как базовый класс Kaspresso childWith. Выше я уточнил, как мы решали проблему повторяющихся ошибок в коде наших тестов с помощью правил. Отдельно можно упомянуть, что большинство моментов бездействия в тестах было связано с тем, что билдер Kaspresso childWith использовался неправильно или вёл себя непредсказуемо. Например, у нас были:
Отмечу, что проблема В то же время мы смогли описать в правиле статического анализа проблему рантайма, упомянутую ранее в главе про RestrictedKeywordRule и связанную с В ближайшем будущем разработчики Detekt планируют выпустить версию 2.0, которая попробует более элегантно решить часть уже решённых нами проблем. Например, в рамках одного правила можно будет добавлять паттерн именования как для классов приложения, так и для тестовых. Также планируется уход от PSI. Кстати, именно поэтому на данном этапе мы не стали писать слишком много правил — только лишь те, которые были очень востребованы. Как написать своё правилоКратко расскажу про основные этапы пути, если вы решили написать своё правило для Detekt. Шаг 1. Защитите идею перед пользователями правила. Если вы создаёте решение, которое будут использовать как минимум несколько десятков человек, то стоит обосновать его необходимость. Шаг 2. Добавьте правило в detekt-config.yml и свой RuleSetProvider. Шаг 3. Напишите первую наивную реализацию правила с использованием visit-методов. При необходимости напишите вспомогательный код. Например, если вам потребовался код для домена тестов, то он может быть таким:
Шаг 4. Напишите юнит-тесты к правилу. Здесь получается интересная картина: юнит-тесты тестируют правило, правило проверяет код автотестов, автотесты тестируют приложение. Если вы внедряете статический анализ в существующую кодовую базу, то на этапе отладки правил она может выступать в качестве большого набора тестовых случаев. После массового исправления нарушений юнит-тесты продолжат покрывать основные случаи. В том числе они будут служить своего рода документацией. Этому также будет способствовать хранение тестовых данных юнит-тестов в виде raw strings с подсветкой синтаксиса через IDE language injection. Шаг 5. Исправьте все нарушения в проекте в соответствии с «правилом бойскаутов». Этот принцип программирования гласит: «Всегда оставляйте код, с которым вы работаете, в более чистом состоянии, чем он был до вас». Шаг 6. Доработайте наивную реализацию правила с учётом нарушений из проекта и юнит-тестов для этого правила. Если рассмотреть пример несложного правила, опустив детали с его подключением, то оно может быть таким:
Issue содержит параметры для отчёта, а у visit-функции есть доступ к исходному коду — она описывает проверку. А так выглядит конфигурация правила в detekt-config.yml.
Способы игнорирования нарушенийЕсли вы внедряете статический анализатор в существующий проект, то на старте вы можете получить десятки, сотни, а то и тысячи нарушений, которые предстоит проработать вашей команде. Некоторые проблемы могут не умещаться в рамки одного Pull Request, поэтому вам понадобятся инструменты для работы с таким техдолгом. Что же нам может предложить Detekt
ОтчётностьDetekt поддерживает отчёты в разных форматах. Вы можете прочитать отчёт программно и открыть блокирующий Issue со списком нарушений в Pull Request. Нам оказалась не нужна вся мощь формата Sarif, и на все проблемы мы открываем один issue из списка нарушений в формате Конечно, в нашем решении приходится копипастить reference, но такая проблема решена в коробочных продуктах. Например, в Qodana — одном из многих полезных инструментов, созданных в JetBrains. Если в вашей предметной области есть типовая ошибка, то рано или поздно она будет допущена — этого не избежать. Сознательность участников процесса работает не всегда, поэтому можно прибегнуть к ограничениям в виде статического анализа. Но при этом важно, чтобы ограничения приносили больше пользы, чем неудобств. Я считаю, что в таком случае уместно лишь незначительное снижение «качества жизни» разработчика, иначе новые инструменты будут вызывать только головную боль и неприязнь. В качестве бонуса прикладываю GitHub-репозиторий с примерами наших правил. Если кого-то заинтересует библиотека из Maven Central, то оставьте комментарий — мы её опубликуем. Полезные материалы |