Введение в культуру DevOps: выбираем стратегию тестирования
Это первая статья из серии «Введение в культуру DevOps». Предыдущий материал был вводным, этот посвящен тестированию. Рассмотрим, какие стратегии тестирования выбрать команде, которая старается культивировать у себя культуру DevOps.
Тестирование и требования
Перед тем как углубиться в выбор стратегии тестирования, давайте подумаем, что такое тестирование в общем. А тестирование как таковое — это всего лишь сопоставление требований заказчика с текущим состоянием продукта. Также тесты можно писать уже на существующий интерфейс/API. И тогда они будут представлять собой слепок с текущего состояния системы, который поможет при ее переделке. Предвидя комментарии, скажу, что ошибки очень часто бывают и в самих требованиях, и их тестирование — отнюдь немаловажная деталь процесса. Но мы предположим, что наш бизнес-аналитик — гений, и он составил идеальные требования, которые доставил нам по радуге на розовом единороге.
Не все тесты одинаково полезны. Если вы тестируете не поведение, а реализацию — тесты будут актуальны очень короткое время, пока реализация не будет заменена. И наоборот, если тестируется поведение модуля, то тесты будут актуальны до тех пор, пока модуль существует. Не стоит писать тесты на приватные методы. Это опять возвращает нас к тому, что мы тестируем поведение public-интерфейсов. Приватные методы — это только детали реализации. С точки зрения тестирования они нам не интересны.
Тесты не должны зависеть друг от друга. Каждый тест должен быть атомарен и ни в коем случае не содержать зависимостей от предыдущих. Если у вас есть такие моменты, то при падении одного теста можно сразу поставить крест на всем тестовом наборе.
Теперь давайте подумаем, какие бывают требования. С точки зрения разработчика, да и инженера по качеству, требования разделяются на два больших класса.
Первый класс — это функциональные требования. Они предусматривают, как должна работать система. Хороший пример функциональных требований — описание того, что при нажатии на кнопку «Submit» должна появляться надпись «Ok». Это требование говорит, что должна делать система. Однако в таких требованиях ничего не сказано про то, как, скажем, быстро должна появиться эта надпись. Поэтому появился второй класс — нефункциональные требования. Они говорят, как это должна делать система. Если приходит требование, что запись должна появляться не менее чем за 1 секунду — это типичное нефункциональное требование. К нефункциональным относится все, что касается юзабилити, производительности, дизайна и т. д.
Теперь перейдем к видам тестирования. И тут я вынужден вставить комментарий от капитана очевидности, что тестирование бывает функциональное и нефункциональное. Также тестирование делится на ручное и автоматическое. Сразу отбросим ручное тестирование. Оно не вписывается в одну из главных практик DevOps «автоматизируй все, что можно». Следовательно, мы будем обращать внимание на автоматические тесты.
Типы автотестов
Когда говорят об автотестах, сразу вспоминаются пирамиды. Пирамида тестирования помогает создать нам тестовый план. Однако она иногда вводит людей в заблуждение, когда дело касается покрытия кода различными видами тестов. Но обо всем по порядку.
Мы будем сравнивать автотесты по следующим параметрам:
- парадигма, исповедуемая тестами Black Box / White Box / Grey box тестирование;
- скорость прохождения сферического единичного теста в вакууме;
- затраты на разработку;
- зависимость от среды исполнения.
Итак, автотесты делятся на следующие типы: Unit, Integration, End-To-End.
Unit
Это тесты, проверяющие public-интерфейсы отдельно взятого модуля. Они исповедуют концепцию white box testing, при которой человеку, который тестирует систему, доступен весь ее исходный код. Сразу оговорюсь, white box в некоторых случаях превращается в полноценный black box из-за того, что иногда модули рассматриваются именно как черные ящики с входящими и исходящими данными.
Единичный тест и тестовые наборы проходят достаточно быстро, что дает возможность разработчику запускать эти тесты фактически после каждого сохранения. В среднем один правильно написанный юнит-тест длится около десятых долей секунды. При этом девелопер может наблюдать в режиме реального времени, что он сломал тем или иным изменением в коде.
Теперь о времени разработки. Как было сказано выше, для тестирования нужен отдельно взятый модуль или функция. Эта функция должна быть изолирована от внешних библиотек и сторонних модулей. Модуль должен быть, по возможности, изолирован от системных таймеров и других взаимодействий с системными API. Это отнимает достаточно много времени на разработку, а, как следствие, юнит-тесты становятся одними из самых трудозатратных при этапе разработке.
Если говорить о затратах на поддержание, юнит-тесты — самые стабильные при прохождении. Среда их выполнения синтетическая и не зависит от внешних факторов. То есть если юнит-тесты упали, то на 99% это ошибка приложения, а не какое-то нелепое стечение обстоятельств, когда что-то пошло не так, как хотелось.
Однако тут кроется момент, который омрачает розовую картину всеобщего юнит-тестирования. Иногда случается, что все тесты проходят, а приложение не работает. Почему? Не стоит забывать, что все модули и функции работают в идеальной синтетической среде, для которой у нас окружение подменено. Они лишь показывают работоспособность ее отдельных частей.
Если подытожить, юнит-тесты — идеальный инструмент проверки качества кода, но не приложения. Они показывают разработчикам, где и что именно они поломали в режиме реального времени. Они должны запускаться на машине разработчика. Также они добавляются в прекоммит хуки или CI pipeline, дабы разрабы не коммитали заведомо нерабочий код.
Integration
Это тесты, проверяющие функциональность взаимодействия нескольких модулей одновременно. Они используют grey-box тестирование, при котором мы можем относиться к тестируемому объекту как к черному ящику и в тоже время дергать какие-то внутренние методы, лазить в базу и т. д.
Положа руку на сердце, практически все тесты, которые создаются разработчиками, являются интегрейшенами. Редко когда разраб упарывается и полностью изолирует систему под тестированием от абсолютно всех взаимодействий. И если API влияют — Integration тесты уже зависимы от окружения. Они уже показывают то, как вы умеете деплоить систему.
Разработка этого вида тестов уже не занимает столько времени, как нужно для Unit`ов. Однако эти тесты более хрупкие, потому что большое количество факторов влияет на их работу. Их прохождение занимает больше времени, нежели у юнит-тестов за счет взаимодействия с третьесторонними API. Теперь уже разработчик не может запускать эти тесты после каждого сохранения, потому как такой тест длится порядка нескольких секунд.
Integration — идеальный способ оценивать то, в каком состоянии сейчас у вас находится билд на CI. Они не требуют много времени и позволяют контролировать то, как работают модули вашей системы, где и какие проблемы у вас вылезут. Integration должны запускаться после каждого пуша в репозиторий, чтобы разработчик мог видеть те непоправимые улучшения в коде, нанесенные его деятельностью.
End-To-End
Это тесты, проверяющие всю функциональность в целом. Такой вид тестов использует концепцию черного ящика, при которой приложение представляет собой неведомую вещь, с которой мы взаимодействуем посредством публичных интерфейсов. Почему-то всегда Е2Е-тесты ассоциируются у большинства разработчиков с Selenium, хотя тестирование REST API средствами http-запросов — также типичный образец Е2Е-тестов, которые полностью укладываются в концепт черного ящика.
Е2Е-тесты проверяют работоспособность системы в целом. Прогон занимает достаточно много времени, поэтому их не гоняют локально. Я бы даже не рекомендовал запускать их после каждого пуша в центральный репозиторий. Тесты идеальны для запуска после слияний веток или относительно окружений sandbox или staging — для того, чтобы контролировать состояние приложения в ветках и на окружениях. Также при использовании микросервисной архитектуры приложения тесты могут покрывать сразу несколько микросервисов, что приведет к лучшему покрытию кода тестами.
E2E-тесты зависят от многих факторов и чрезвычайно хрупкие. Иногда письмо о заваленном билде может прийти только из-за того, что затупил браузер или же была проблема со связью на виртуалке (кстати, это утверждение справедливо и для Integration).
E2E хороши для проверки того, что все заехало хорошо. То есть если стоит задача проверить систему на работоспособность, E2E — это то, что для вас нужно. Однако помните, что довольно часто будут случаться ложные тревоги.
Покрытие кода тестами
Если у вас один тест, который покрывает одну не использующуюся нигде функцию, то проку от него — ноль. Если же тесты покрывают все части приложения, то вы можете быть уверены, что вероятность внезапных сюрпризов минимальна.
Теперь давайте обсудим проценты покрытия тестов. Почему-то покрытие принято считать по линиям кода. То есть у нас есть 100 линий кода, тест (не важно какого типа) проходится по
Существуют такие метрики покрытия кода тестами:
- По линиям. Описано выше.
- По классам или методам. Класс или метод считается покрытым, если он хотя бы раз был вызван тестами. Эту метрику любят менеджеры, потому что обычно она показывает самые впечатляющие результаты.
- По логическим веткам. Ветка считается покрытой, если в нее хоть раз заходил тест. То есть если у вас есть if с else — и тест попал только в одну из его логических ветвей, то покрытие такого кода будет равняться 50%. Эта метрика очень удобна для оценки того, насколько детально вы просчитали все варианты развития событий в ваших тестах. Имейте в виду, что ветками считаются if-else, switch-case, try-catch. Тернарники не считаются элементами, разветвляющими код.
- По выражениям. Выражение считается покрытым, если оно выполнялось хоть раз при проходе тестом. Вот тут и считаются тернарники, так как они идут на одном уровне с обычными операторами.
Покрытие кода тестами показывает уровень того, насколько ваши тесты гарантируют работоспособность системы при их успешном прохождении. Это не гарантия того, что все будет хорошо, но с большой долей вероятности все будет в шоколаде.
DevOps и стратегия тестирования
DevOps-культура поощряет частые релизы. Частые релизы — это страховка от поломки вашего приложения после непоправимых улучшений разработчиками, так как за раз вы будете вывозить меньше изменений. Однако частые релизы требуют больше QA-работы. То есть если вы будете выливаться чуть ли не каждый час, то ваши инженеры по качеству просто перестанут ходить домой, есть, пить и вести социальную жизнь.
DevOps-культура предлагает свести к минимуму ручную QA-работу, дабы иметь возможность релизиться как можно чаще, что безопаснее и стабильнее. В DevOps-культуре роль QA-инженеров смещается от тестеров к людям, которые следят за качеством проекта и помогают разработчикам в написании автоматических тестов, вырабатывают стратегию тестирования. Тут уже мало места для QA, которые не разбираются в технологических аспектах приложения, CI/CD процессах.
Стратегия тестирования — это план, который позволит вам работать с минимальной затратой времени, а следовательно, и денег. Стратегия тестирования для DevOps не должна звучать: «Мы используем Protractor и Jenkins». Это не план. Это всего лишь перечисление инструментов, которые используются вами. Главное вопросы, на которые должен отвечать план, — почему и зачем.
Мы должны ответить себе на следующие вопросы:
- Какие ресурсы у нас обязательны к тестированию?
- Каков будет результат успешного прохождения тестов? Какую информацию нам надо получить для успешной поставки или что может остановить поставку и заставить нас начать что-то поправлять?
- Риски. Как наши тесты уменьшают их? И как это будет представлено команде?
- Расписание. Когда мы будем прогонять те или иные виды тестов?
- Кто будет отвечать за автоматизированные тесты? Кто будет их разрабатывать? Кто будет конечным получателем результатов тестов?
- Что будет триггерить старт тестов? Когда команде ожидать начала тестирования?
Первое, с чего мы должны начать, — это внедрить концепцию TDD. Сразу скажу, что TDD не является абсолютно правильным выбором, но оно способствует написанию тестов как таковых. То есть разработчики должны писать тесты еще до начала кодирования. У разработчиков любое действие сохранения должно триггерить запуск юнит-тестов. Кроме того, инженер не должен иметь возможности залить свой код с поломанными тестами или когда тестовое покрытие падает ниже какого-то предела.
При пуше кода в фича бранч должны прогоняться сьюты integration- и unit-тестов. Они определяют статус билда и возможность смерджить его в главную dev-ветку. При поломанном билде не должно быть возможности мерджа, а коммитер должен быть оповещен, что его коммит сломал ветку. Далее идет мердж в master или stage brunch и сборка там. При этом требуется прогонять все тестовые наборы, включая E2E.
Этот тестовый набор определяет, может ли осуществляться поставка вашего продукта. В случае фейла все члены dev-команды должны быть оповещены о том, что поставка отложена, и о причине, вызвавшей данную ошибку.
Если все наборы на тестовых окружениях прошли нормально, осуществляется поставка продукта на продакшен и снова прогон E2E тестовых наборов уже на продакшен окружении. В дополнение к вышеперечисленным тестовым наборам на продакшене должен быть осуществлен прогон performance, penetration и других нефункциональных тестов. Если что-то пошло не так — осуществляется откат и оповещение всех задействованных в процессе деплоймента и разработки. Причем заметьте, что откат должен быть тоже протестирован E2E и другими тестами.
Как видим, каждый раз мы прогоняем все более и более ресурсоемкие тесты, при этом отсекая ошибки, которые могут быть связаны с разными факторами. У вас образуется тестовый пайплайн, при котором ваш продукт переходит от одних тестов к другим.
Выводы
Автоматизированное тестирование — одна из неотъемлемых практик DevOps-культуры. Она позволяет контролировать исходный продукт и оперативно устранять ошибки, начиная от процесса разработки и заканчивая процессом деплоя на продакшен-окружение.
Также хочу сказать большое спасибо всем, кто оставляет комментарии на мои статьи, всем людям, которые принимают активное участие в обсуждении этой серии статей.