Экскурс в «святая святых» ОК: как мы пишем и ревьюим код автотестов

30baa6afc909538059827b1393329ac8.jpg

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

Делимся опытом работы с автотестами в ОК: от подходов к постановке задач до схемы настройки окружения статического анализатора кода.

Материал подготовлен по мотивам доклада руководителя команды автоматизации тестирования ОК Эмилии Куцаревой и младшего инженера по автоматизации тестирования соцсети Евгения Буровникова на ИТ-конференции «Стачка».

Немного контекста: автотесты в ОК

ОК — одна из самых популярных социальных сетей в рунете, которая представлена на всех возможных платформах (web, mobile web, API, android, iOS). Наш продукт высоконагружен, имеет сложный бэкенд и сотни сервисов. Например, у него «под капотом»: 50 тысяч Docker-контейнеров, 1 эксабайт данных и обработка данных в 7 дата-центрах.

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

Так, сейчас у нас более 10 тысяч автотестов:

  • web — около 3150;

  • API — около 2500 (+1500 автосгенерированных);

  • Android — около 1400;

  • mobile web — около 1200;

  • iOS — около 950.

При этом у нас всего около 50 человек в командах обеспечения качества и автоматизации тестирования. 

Такое соотношение количества тестировщиков к количеству автотестов задает высокие требования к качеству их написания — нам важно, чтобы с таким массивом автотестов можно было справиться доступными ресурсами команд. Поэтому мы выстраиваем эффективные процессы и используем различные инструменты для автоматизации работы. Теперь обо всем последовательно и подробно.

Постановка задач на автотесты

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

Чтобы технический долг на покрытие автоматизации у нас был регламентирован, все задачи на автотесты создаются в Jira и также проходят свой workflow. Задачи на покрытие автотестам или их исправление создаются из разных источников.

  • С помощью отслеживания кода. Например, задачи на покрытие автотестами новых методов API — добавление метода в релизную ветку отслеживается через код с помощью сборки в TeamCity и так автоматически создается задача на покрытие автотестом.

  • С помощью workflow Jira. Например, задачи на покрытие автотестами при закрытии задачи на разработку — после завершения разработки фича передается на покрытие автотестом. При этом, мы делаем это в полуавтоматическом режиме — QA инженер или другой специалист сам выбирает, нужно ли покрывать фичу автотестом.

Причем из Jira, помимо основных операций с задачей, сразу можно создать задачу на эксперимент и автотест или просто автотест. Таким образом процесс можно настроить под нужды продукта.

ca44c096bf85c472009dfd55cddb4617.jpeg

  • Из системы репортинга автоматизированных тестов. Например, задачи на исправление падающих автотестов. У нас есть внутренняя система репортинга, с помощью которой мы можем отслеживать, в том числе, состояние автотестов. Если автотест падает, система создает задачу, а бонусом является то, что прилинковывается не только обнаруженное падение, но и все похожие. 

  • С помощью внутренних инструментов автоматизации тестирования. Например, у нас есть сервис по оценке покрытия автоматизированными автотестами API платформы, из которого мы можем получить приоритетные для покрытия задачи.

  • Исходя из бизнес-логики. Например, работу над задачами по методам приватности API мы пока не автоматизировали, но также отслеживаем качество покрытия подобных кейсов. Фактически в этом случае мы изучаем методы и вручную создаем задачи на QA-инженеров. 

Двухуровневое ревью

У нас в ОК принята практика двухуровневого ревью кода автотестов. Мы внедрили её в Jira и Bitbucket. 

Двухуровневое ревью в Jira

Для наших web, mobile web и API-платформ ревью состоит из двух этапов.

  • Первый этап — проверка продуктовым QA. Такое ревью внутри команды помогает проверить качество написанного кода с точки зрения тестового кейса и регламентированных проверок.

  • Второй этап — ревью командой автоматизации и экспертами по автоматизации из продуктовых команд. 

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

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

Примечание: Привлечение к проверкам специалистов из продуктовых команд позволяет нам сократить риски, повысив bus factor и активно развивать внутреннюю экспертизу. 

Двухуровневое ревью в Bitbucket

Аналогичным образом для web, mobile web и API-платформ выстроено ревью в Bitbucket. Ревьюеры назначаются в соответствии с задачей в Jira.

Также мы используем подход с мейнтейнерами по пакетам (по аналогии с разработкой). В нашем случае пакеты соответствуют продуктовым сервисам, за которые отвечают определенные QA-инженеры. Суть в том, что действия с ключевой частью кода проекта автотестов должен согласовать ответственный за эту определенную часть кода продуктовый QA — без этого залить изменения в основную ветку не получится. Так мы делаем процессы более контролируемыми и снижаем риск человеческой ошибки.

На практике это выглядит следующим образом: мы видим в Bitbucket, что не можем залить изменения кода автотестов, пока не получим минимум один аппрув от неких Юлии или Владислава, так как именно они отвечают за измененную нами часть кода.

3107f322bf051cda0eae7a3622e12bf8.jpeg

Ревью на Android и iOS

Ревью автотестов для Android и iOS несколько отличается, поскольку в этих командах у нас есть core QA-инженеры, которые активно участвуют в процессах автоматизации тестирования.

  • Первый этап — проверка продуктовым QA.

  • Второй этап — ревью специалистом из команды автоматизации и/или core QA команды Android или iOS. Такое комбинирование проверяющих помогает нам делиться уникальной экспертизой в автоматизации и ревью тестов на платформе.

Таким образом, при выстраивании процессов ревью мы учитываем доступные ресурсы, экспертизу и текущую нагрузку на специалистов.

Запуски автотестов

Мы обязательно запускаем автотесты — на платформах web, mobile web и API в pull request для прохождения ревью нужно приложить отчет об успешном запуске автотестов, код которых был задет. Причем автотесты обязательно проверяются на корректность работы как на тестовой среде, так и на прод. В этом помогает работа с облаком — благодаря масштабируемым облачным ресурсам, мы можем запускать автотесты параллельно и в большом количестве. 

На Android-платформе у нас в pull request автоматически запускаются все UI-автотесты на каждый commit и по результатам этого запуска происходит автоматическое разрешение или запрет мерджа. В этом также помогает облако — мы можем одновременно запускать до 750 автотестов. То есть весь объем автотестов на Android (у нас их около 1400) мы можем прогнать всего за 7–10 минут. И даже с учетом автоматических перезапусков на это нужно не более 15 минут. 

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

Стиль кода автотестов

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

  • общая работа с кодом;

  • логика классов пейджей (страниц или экранов);

  • логика тестовых классов.

Правила общей работы с кодом

  • Использование единых языков: Java и Swift (для iOS).

  • Четкое следование «архитектуре» в именах переменных, методов, классов, пакетов. Это нужно для упрощения поиска нужных компонентов, улучшения человекочитаемости и интерпретируемости их названий.

  • Форматирование кода тестовых классов, пейджей и не только строго с учетом выработанных внутренних правил или с использованием согласованных шаблонов.

  • Обязательное использование описания методов (в формате JavaDoc) или понятные и однозначные названия методов.

  • Вынесение всевозможных данных в константы для исключения ситуаций с дублированием кода.

  • Ограниченное использование циклов while с обязательным пределом выполнения по тайм-ауту. 

  • Ограничение размеров тестового класса для поддержания атомарности. В нашем случае — не более 300 строчек.

Логика пейджей

  • Использование паттерна Loadable Component, который стандартизирует обработку загрузки и проверки состояния объектов страниц. Благодаря этому, мы можем проверять базовые компоненты в конструкторе и логировать проблемную точку входа. Это помогает нам выявлять падения сразу и локализовать момент сбоя.

  • Использование паттерна Steps (вместе с тем уход от паттерна Helper). Это позволяет нам во время теста проверять отдельные фрагменты пользовательских сценариев и не дублировать код.

  • Обязательное добавление проверок с логированием перед действиями с элементами. Это делает действия более прозрачными и помогает автоматически отлавливать ошибки вроде отсутствия кнопки или неправильной загрузки отдельных полей.

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

Логика тестовых классов

  • Наличие проверок в тестовых классах — очевидное, но обязательное правило.

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

  • Обязательная подготовка и зачистка данных, например, с помощью разных аннотаций — @BeforeEach и @AfterEach.

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

  • Гибкое изменение условий написания тестовых методов. По умолчанию на каждый тестовый класс мы пишем один метод. Но при необходимости мы допускаем написание нескольких тестовых методов в классе — для этого достаточно учесть некоторые специализированные рекомендации.

  • Использование динамических и статических ботов в зависимости от тестовых кейсов. Например, автотесты для Android мы запускаем предпочтительно на динамических ботах — из более чем 1400 тестов лишь порядка 40 выполняются на статических ботах на редактирование. 

Это лишь часть важных правил из всего используемого нами стиля кода — в действительности их значительно больше.

Примечание: В нашем случае стиль кода автотестов для разных платформ (например, Android и iOS) отличается. Во многом это обусловлено необходимостью учитывать платформенную специфику и подстраиваться под особенности фреймворков.

Статический анализатор кода

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

Из большого стека доступных вариантов для этих задач мы используем PMD — статический анализатор, который выявляет типичные ошибки разработки: неиспользуемые переменные, пустые блоки, создание ненужных объектов и другие проблемы.

Причин в пользу выбора именно этого решения несколько:

  • быстроразвивающийся проект с активным комьюнити;

  • бесплатный инструмент с открытым исходным кодом;

  • поддержка Java и других языков программирования;

  • интеграция с Bitbucket и Teamcity;

  • наличие большого набора готовых правил;

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

PMD можно подключать по-разному. Например, как:

  • плагин для Maven, Gradle, Ant;

  • часть инструмента автоматизированной проверки кода codacy, codiga;

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

Мы используем PMD именно как зависимость для проекта.

Структура PMD проекта

Используя PMD, мы построили свой проект, который получил название ODKL AT PMD. Он состоит из четырех основных частей:

  • Startup Configuration. Конфигурации запуска, которые мы получаем в качестве внешней зависимости в удобном для нас виде. Конфигурации парсятся, после чего происходит настройка запуска и работы линтера. 

  • Rulsets. Представляют собой набор (список) правил. Хранение правил в одном проекте дает нам возможность их централизованного изменения.

  • Custom rules. Кастомные правила, которые позволяют учитывать специфику работы с нашим проектом.

  • Test for custom rules. Тесты для кастомных правил, которые нужны для проверки логики их работы и защиты от ложных срабатываний линтера. 

655f2b748c6466c329097f996e62d20b.jpeg

Подробнее о правилах

Самое важное и интересное в нашем проекте — правила. 

Примечание: В статье мы приводим правила, которые актуальны для PMD версии 7.0.0.

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

Абстрактное синтаксическое дерево (AST) — дерево, отражающее структурные связи между существенными элементами исходного выражения, но не отражающее вспомогательные языковые средства (отступы, комментарии и другие). AST создается парсером по мере синтаксического разбора программы. Причем нам не приходится заботиться по поводу парсера — в AST он и так есть.

На уровне кода абстрактно-синтаксическое дерево имеет примерно следующий вид:

6fef2932cbc28e0ebbe64a7ae64eb3c9.jpeg

Схема работы с данными выглядит следующим образом:

  • тестовые классы парсятся парсером и преобразуются в абстрактно-синтаксическое дерево;

  • абстрактно-синтаксическое дерево на основе правил из rulsets преобразуется в результат.

4fc5b6956d8554179d45594c61d89bbf.jpeg

Реализация правил

eaff127a7f8ad068dfa751f2f873c521.jpeg

В зависимости от языка, реализацию правил можно условно разделить на две группы. Рассмотрим несколько простых примеров.

Пример XPath правила

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

2b28287eba1e1a6978bbee0799df4155.jpeg

Здесь мы указываем:

  • наименование правила;

  • язык, для которого пишется правило;

  • сообщение, которое выводится при нарушении правила;

  • стандартный class для всех XPath-правил;

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

  • приоритет (PMD позволяет назначать пять уровней приоритета, где 1 — самый высокий, 5 — самый низкий);

  • ядро правила — XPath-выражение, с помощью которого мы ищем определенные синтаксические конструкции и делаем выводы.

Пример Java правила

Java правила позволяют реализовать как простую (как в примере ниже), так и более сложную логику. Например, нам нужно, чтобы все тесты имели тег. На уровне кода соответствующее правило будет иметь следующий вид:

d13e6d786a42d052c7439946717d2343.jpeg

Здесь мы:

  • создаем класс и наследуем его от класса AbstractJavaRule;

  • прописываем блок необходимых параметров и констант;

  • в соответствии с паттерном visitor описываем логику обхода определенных типов узлов нашего абстрактно-синтаксического дерева.

В данном случае мы проверяем, является ли класс тестовым. Если нет — завершаем анализ, если да — продолжаем анализ и переходим к узлам другого типа (например, ASTMethodDeclaration). Далее мы анализируем тестовые функции и ищем у них аннотации, которые отвечали бы за тег.

Схема настройки окружения

Теперь подробнее о схеме настройки окружения. 

В нашем случае она имеет следующий вид:

  • В проекте с web тестами тестировщик делает commit и отправляет его на удаленный репозиторий (в нашем случае — Bitbucket).

  • Далее Bitbucket фиксирует все изменения и передает об этом информацию TeamCity.

  • TeamCity, в свою очередь, начинает пересборку проекта. Если сборка неуспешная — вся информация прокидывается обратно в Bitbucket. Если все удачно — начинается подготовка к анализу кода. 

  • Следующим идет этап подготовки к анализу: мы получаем полный список файлов, которые надо проанализировать, и актуальную версию jar файла (нашего проекта с PMD).

  • Далее происходит запуск jar файла и начинается анализ кода.

  • Результат работы линтера передается нашему специальному скрипту. Он выделяет результаты анализа не всего файла, а только измененного кода (сам по себе PMD не умеет анализировать отдельные фрагменты), и передает их в Bitbucket.

  • Информация с результатами анализа в Bitbucket доступна тестировщику.

57fdea00ab8ef98c5c6e8fdcc1c173a9.jpeg

В своем pull request в Bitbucket мы получаем весь массив нужной информации. В том числе полный отчет о работе линтера с приоритетами недочета, информационными сообщениями, указанием проблем и места, где было нарушено правило (если такая ошибка случилась).

Преимущества PMD

Мы выделили несколько основных достоинств PMD конкретно для наших сценариев использования.

  • Получение быстрого фидбека по коду при создании pull request.

  • Увеличение эффективности работы ревьюера — он может заниматься более детальным анализом кода, а не поиском одних и тех же недочетов.

  • Повышение качества кода за счет автоматизации процесса ревью.

  • Сокращение времени, затрачиваемого на исправление ошибок в pull request, и уменьшение количества итераций возвращения кода на ревью.

Вместо заключения

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

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

Поэтому мы стремимся уделять внимание не только разработке регламентов и выбору инструментов, но и повышению культуры работы с автотестами в целом. Чтобы добиться этого, мы ставим во главу угла специалистов: их запросы, экспертизу и ресурсы. Как результат, наш путь к улучшению кода автотестов и оптимизации работы с ними эволюционный, «не обременен излишней бюрократией» и прозрачен для всех вовлеченных специалистов.

© Habrahabr.ru