Что такое пирамида тестирования
От пирамиды тестов – к колесу автоматизации: какие проверки нужны на проекте
О задачах автоматизации тестирования и случаях, когда она необходима, мы уже писали на Хабре. А для выбора необходимых проверок удобно иметь под рукой наглядное пособие, не ограничиваясь знаменитой пирамидой автотестов. Предлагаем перевод статьи Кристин Джеквони (Kristin Jackvony), где графически показан еще один метод – колесо автоматизации.
Автоматизация тестирования, как правило, наиболее необходима в масштабных приложениях с большим количеством бизнес-функций, при внедрении CI/CD и регулярных релизов. Подробнее об этом мы рассказывали в статье «Что даёт автоматизация тестирования».
С разрешения Кристин Джеквони – автора блога Think Like a Tester и ряда популярных материалов о тестировании – мы перевели статью «Переосмысление пирамиды автотестов: колесо автоматизации» (Rethinking the Pyramid: The Automation Test Wheel). В конце статьи рассмотрим пример проверок из практики наших специалистов по автоматизации тестирования (SDET).
Каждый, кто занимался автотестами, слышал о Пирамиде автотестов. Обычно она имеет три слоя: тесты UI, API и Unit. Нижний слой, самый широкий, занимают Unit-тесты – это означает, что таких тестов должно быть больше, чем прочих. Средний слой – это API-тесты; идея в том, что их нужно запускать меньше, чем Unit-тестов. Наконец, верхний слой – это UI-тесты. Их должно быть меньше, чем остальных, т.к. они требуют больше времени на прохождение. Кроме того, UI-тесты наиболее нестабильные.
Пример пирамиды автотестов на Хабре. Метод описан в книге Майка Кона «Scrum: гибкая разработка ПО» (Succeeding With Agile. Software Development Using Scrum).
С моей точки зрения, в этой пирамиде две проблемы:
Я предлагаю другой подход к автоматизированному тестированию – Колесо автоматизации.
Каждый из типов тестов можно рассматривать как спицу в колесе. Нет спицы, которая была бы важнее других – они все необходимы. Размер сектора колеса не означает число тестов, которые должны быть автоматизированы. При этом нужно предусмотреть проверки по каждому указанному направлению, если в вашем проекте требуется обеспечивать качество в соответствующей области.
Unit-тесты (Unit Tests)
Unit-тесты – это самые маленькие автотесты, какие только возможны. Они проверяют поведение только одной функции или одного метода. Например, есть метод, который проверяет, равно ли число 0, тогда можно написать такие Unit-тесты:
Компонентные тесты (Component Tests)
Эти тесты проверяют разные сервисы, от которых зависит код. Например, если мы используем API GitHub, то можно написать компонентный тест для проверки того, что запрос на данный API получает ожидаемый ответ. Или это может быть просто пинг до сервера, проверка доступности базы данных. Должен быть хотя бы один компонентный тест на каждую используемую в коде службу.
Примечание: например, у нас в SimbirSoft к компонентным тестам, помимо проверки ответов сторонних сервисов, относятся проверки модулей одной системы, если её архитектура представляет собой набор таковых.
Сервисные тесты (Services Tests)
Такие тесты проверяют доступность веб-сервисов, к которым обращается приложение. Чаще всего работа с ними организована через использование API запросов.
Например, для API с типами запросов POST, GET, PUT и DELETE мы можем написать автотесты на проверку каждого типа запросов. Причем наши тесты будут проверять как позитивные, так и негативные типы сценариев, при которых некорректный запрос возвращает соответствующий код ошибки.
Тесты пользовательского интерфейса (User Interface Tests, UI Tests)
UI-тесты проверяют правильность реакции системы на действия конечного пользователя. К таким проверкам относятся, например, тесты, проверяющие ответ системы на заполнение текстовых полей или на нажатие кнопок.
Как правило, любая функциональность системы, которая может быть протестирована с помощью Unit, компонентного или сервисного теста, должна быть проверена с использованием упомянутого типа теста. Тесты же пользовательского интерфейса должны быть сосредоточены исключительно на выявлении ошибок во взаимодействии пользователя с графическим интерфейсом.
Визуальные тесты (Visual Tests)
Визуальные тесты проверяют отображение элементов интерфейса на экране. Они схожи с UI-тестами, но фокусируются на проверке внешнего вида интерфейса, а не на функциональности. Примерами таких проверок могут быть проверка правильности отображения изображения кнопки или проверка отображения логотипа продукта на экране.
Тесты безопасности (Security Tests)
Это тесты, проверяющие соблюдение мер безопасности и защиты данных. Они по механике похожи на сервисные тесты, но тем не менее рассматривать их следует отдельно. Например, тест безопасности может проверить, что токен авторизации не будет создан при недопустимой комбинации логина и пароля. Другой пример – создание GET-запроса с токеном авторизации для пользователя, не имеющего доступа к этому ресурсу, и проверка того, что будет возвращен ответ с 403 кодом.
Примечание: с сервисными тестами эти проверки роднит только способ вызова служб – через http-запросы. Цель таких тестов – исключительно в проверке защищенности системы и пользовательских данных от действий злоумышленника.
Тесты производительности (Performance Tests)
Автотесты производительности проверяют, что ответ на запрос приходит в рамках соответствующего периода времени. Например, если есть требование к системе, что GET-запрос никогда не должен занимать дольше двух секунд, то при превышении этого ограничения автотест вернет ошибку. Время загрузки web-интерфейса также можно измерять с помощью тестов производительности.
Тесты доступности (Accessibility Tests)
Автотесты проверки доступности служат для проверки самых разных вещей. В сочетании с тестами пользовательского интерфейса они, например, могут проверять наличие текстового описания изображения для слабовидящих. В рамках проверки доступности визуальные тесты могут быть использованы для проверки размера текста на экране.
Как выбрать методы проверки
Возможно, вы заметили, что приведенные выше описания тестов часто пересекаются друг с другом. Например, тесты безопасности могут выполняться в процессе тестирования API (сервисные тесты), а визуальные тесты – в процессе тестирования пользовательского интерфейса. Здесь важно, чтобы каждая область была проверена тщательно, эффективно и точно.
После выхода статьи «Переосмысление пирамиды автотестов: колесо автоматизации» (Rethinking the Pyramid: The Automation Test Wheel) вышел список факторов, помогающих определить необходимую долю тестов каждого типа в колесе автоматизации:
Из практики SimbirSoft
Мы считаем, что колесо автоматизации – полезный способ визуализации типов тестирования. Безусловно, мы давно используем описанные тесты, но картинка помогает проанализировать покрытие системы и держать всё перед глазами.
Если у вас отлажен процесс тестирования и вы регулярно прогоняете автотесты, это минимизирует риск попадания дефектов в продакшен. К тому же, автотесты помогают оперативно выявлять проблемы, которые могут возникнуть в процессе работы системы. Например, с помощью планового ежедневного прогона автотестов можно контролировать свой сайт, чтобы защитить себя от взломов.
Какие использовать проверки и в каком количестве – зависит и от требований, и от самой проверяемой системы. Так, в одном из наших продуктов – банковском мобильном приложении – по требованию клиента были подготовлены автотесты всех перечисленных ранее видов. Рассмотрим их примеры далее.
Unit Tests
Мы позаботились о том, чтобы каждая функция в коде приложения была покрыта тестами, проверяющими все возможные пути. При этом активно использовали моки, заменяющие собой другие части системы. Например, проверяя функцию конвертации рублей в валюту, мы подавали на вход функции поддерживаемые ей рубли, чтобы проверить, что на выходе получим валюту. Сравнение происходило с эталонными величинами. Также проверяли реакцию функции на невалидные данные.
Component Tests
Здесь мы проверяли компоненты мобильного банка. Так как у нас микросервисная архитектура, нужно было проверить доступность всех микросервисов, а также, как пример, доступность базы данных. Для сервиса, ответственного за расчетные счета клиента, мы выполняли запрос к базе данных для получения расчетного счета и проверки того, что счет может быть получен.
Services Tests
Мы проверяли API, через которое в предыдущем примере отправляются запросы к базе данных. Отправляли GET-запрос на получение номера расчетного счета и проверяли ответ на него. Обязательно отправляли какой-либо некорректный запрос, например, с несуществующим id клиента, и проверяли, что ответ соответствует ожидаемому.
UI Tests
Например, мы проверили корректную реакцию системы на заполнение полей счета, а также ответ системы на нажатие кнопки «Открыть вклад».
Visual Tests
Мы протестировали устройства с разными версиями iOS и Android и диагональю экрана и проверили расположение элементов приложения. Эта проверка была нужна, чтобы убедиться, что при любом сочетании этих параметров кнопки не наезжают на логотип и не скрываются за ним, становясь недоступными для нажатия.
Security Tests
Мобильный банк – финансовая система, поэтому его безопасность нужно было обеспечить на всех уровнях. В числе прочего мы проверяли аутентификацию только по правильным данным на уровне пользовательского интерфейса и получение информации только в ответ на запрос с корректным токеном на уровне сервисов.
Performance Tests
Одним из тестов было сравнение с эталоном времени ответа сервера на запрос приложения. Также мы проверили работу приложения при максимально ожидаемом количестве одновременно работающих пользователей в системе.
Accessibility Tests
Проверили голосовой поиск и отображение элементов разметки с активированным режимом для слабовидящих.
О том, как можно организовать автоматизацию тестирования с нуля, мы расскажем подробнее в одной из следующих статей.
Спасибо за внимание! Надеемся, что перевод был вам полезен.
Как встроить качество в процессы производства ПО? (Часть 2)
В предыдущей статье Как встроить качество в процессы производства ПО? мы коснулись основных понятий про качество, четырехуровневый процесса управления и обеспечения качества, увидели что требования и качества тесно связаны друг с другом.
STLC и SDLC живут параллельно
Этап 1. Анализ требований. На данном этапе группа тестирования изучает требования с точки зрения тестирования, чтобы идентифицировать тестируемые требования, а группа QA может взаимодействовать с различными заинтересованными сторонами для детального понимания требований. Требования могут быть функциональными или нефункциональными. На этом этапе также выполняется технологическое и экономическое обоснование автоматизации тестирования.
Этап 3. Разработка тест кейсов. Этап разработки тест кейсов включает в себя создание, проверку и переработку тест кейсов и тестовых сценариев после того, как план тестирования будет готов. В первую очередь идентифицируются тестовые данные, затем создаются и проверяются, а затем переделываются на основе предварительных условий. Затем команда QA начинает процесс разработки тест кейсов для отдельных модулей.
Этап 4. Настройка тестовой среды. На данном этапе определяются программные и аппаратные условия, при которых продукт будет тестироваться. Это один из важнейших аспектов процесса тестирования, который может выполняться параллельно с этапом разработки тест кейсов.
Этап 5. Выполнение тестов. На этом этапе работают тестировщики, которое тестируют сборку программного обеспечения на основе подготовленных планов тестирования и тест кейсов.
SDLC и STLC работают параллельно и в этих процессах участвуют все и не только разработчики и тестировщики. Чтобы вся команда работала над встраиванием качества в процессы, в продукт, надо работать с мышлением и менять его. Команда должна понимать, что качественное тестирование очень важно для бизнеса, так как цена исправления пропущенных и обнаруженных ошибок на продуктивных средах может оказаться катастрофически высока (такие ошибки могут ударить не только по финансам, но и по репутации).
Относительная стоимость исправления ошибок в зависимости от времени и места обнаружения
Очень важен выбор типов тестирования. Существует как минимум 105 видов тестирования программного обеспечения. Но, естественно, все применять на продукте и в процессах не надо. Наоборот, список функциональных и не функциональных типов тестирования надо формировать обдуманно. Список должен включать в себя 4 категории тестов
Тесты, которые направляют разработку, заставляя команду разработчиков думать о том, как они будут тестировать story или фрагмент кода, прежде чем писать его.
Тесты ориентированные на бизнес и/или технологии. Написанные с использованием бизнес-терминологии, бизнес-тесты должны быть понятны пользователю. Написанные на языке разработчика, технологические тесты используются для оценки того, обеспечивает ли система поведение, задуманное разработчиком.
Тесты критикующие решение путем оценки системы на соответствие требованиям пользователя, чтобы найти дефекты или отсутствующие (нереализованные) фичи.
Квадранты тестирования
Некоторые из этих тестов должны быть автоматизированы, некоторые должны запускаться при помощи разных тулзов, а некоторые должны быть ручными.
Важно поменять у команды мышление и отношение к обеспечению качества. Существует такое понятие, подход как Test-First Thinking. Давайте ответим на простой вопрос. Для чего вообще тестирование? Для того, чтобы получить некую обратную связь по тому что было сделано, реализовано.
В традиционной V-модели, когда вначале описывается фича, потом userstory, потом пишется код. А после этого начинается тестирование кода, тестирование userstory, тестирование фичи.
Традиционная V-модель тестирования. Медленная ОС!
Как видно в такой модели очень медленная обратная связь. А медленная обратная связь тормозит весь процесс создания ценности для клиента. Последствия этого думаю описывать не надо.
C подходом Test-First Thinking и Shift left testing ситуация меняется. Мы получаем очень быструю обратную связь. Как только появляется достаточно реализации, можно запускать тесты для проверки написанного кода, для проверки userstory и фичи. То есть тестирование идёт постоянно! Как результат быстрая обратная связь. Подход Test-First Thinking для кода означает, что ни строчки кода, пока не написан тест и не тестировать код, а кодировать для теста. А для фич и userstory Test-First Thinking реализуется BDD.
Быстрая ОС!
Пирамида тестирования и ее анти-паттерн
Резюмируя скажу, что первый шаг на пути к встраиванию качества в процессы, начинается с работы по смене мышления у команды, и конечно же, аудита тех тестов, которые есть на продукте. Аудит покажет реальную картину с пирамидой тестирования, далее внедрять концепцию Test-First Thinking и Shift left testing.
Пирамида тестов на практике
Об авторе: Хэм Фокке — разработчик и консультант ThoughtWorks в Германии. Устав от деплоя в три ночи, он добавил в свой инструментарий средства непрерывной доставки и тщательной автоматизации. Сейчас налаживает такие системы другим командам для обеспечения надёжной и эффективной поставки программного обеспечения. Так он экономит компаниям время, которое эти надоедливые людишки тратили на свои выходки.
«Пирамида тестов» — метафора, которая означает группировку тестов программного обеспечения по разным уровням детализации. Она также даёт представление, сколько тестов должно быть в каждой из этих групп. Несмотря на то, что концепция тестовой пирамиды существует довольно давно, многие команды разработчиков по-прежнему пытаются неправильно реализовать её на практике должным образом. В этой статье рассматривается первоначальная концепция тестовой пирамиды и показано, как её воплотить в жизнь. Она показывает, какие виды тестов следует искать на разных уровнях пирамиды, и даёт практические примеры, как их можно реализовать.
Перед выпуском программное обеспечение нужно тестировать. По мере взросления софтверной отрасли созрели и подходы к тестированию. Вместо мириадов живых тестировщиков разработчики перешли к автоматизации большей части тестов. Автоматизация тестов позволяет узнать о баге в считанные секунды и минуты после его внесения в код, а не через несколько дней или недель.
Резко сокращённый цикл обратной связи, подпитываемый автоматизированными тестами, идёт рука об руку с гибкими практиками разработки, непрерывной доставкой и культурой DevOps. Эффективный подход к тестированию обеспечивает быструю и уверенную разработку.
В этой статье рассматривается, как должен выглядеть хорошо сформированный набор тестов, чтобы быть гибким, надёжным и поддерживаемым — независимо от того, создаете ли вы архитектуру микросервисов, мобильные приложения или экосистемы IoT. Мы также детально рассмотрим создание эффективных и удобочитаемых автоматизированных тестов.
Важность автоматизации (тестов)
Программное обеспечение стало неотъемлемой частью мира, в котором мы живём. Оно переросло первоначальную единственную цель увеличить эффективность бизнеса. Сегодня каждая компания стремится стать первоклассной цифровой компанией. Все мы каждый день выступаем пользователями всё большего количества ПО. Скорость инноваций возрастает.
Если хотите идти в ногу со временем, нужно искать более быстрые способы доставки ПО, не жертвуя его качеством. В это может помочь непрерывная доставка — это практика, которая автоматически гарантирует, что ПО может быть выпущено в продакшн в любое время. При непрерывной доставке используется конвейер сборки для автоматического тестирования ПО и его развёртывания в тестовой и рабочей средах.
Вскоре сборка, тестирование и развёртывание постоянно растущего количества ПО вручную становится невозможной — если только вы не хотите тратить всё своё время на выполнение вручную рутинных задач вместо доставки рабочего софта. Единственный путь — автоматизировать всё, от сборки до тестирования, развёртывания и инфраструктуры.
Рис. 1. Использование конвейеров сборки для автоматического и надёжного ввода ПО в эксплуатацию
Традиционно тестирование требовало чрезмерной ручной работы через развертывание в тестовой среде, а затем тестов в стиле чёрного ящика, например, кликанием повсюду в пользовательском интерфейсе с наблюдением, появляются ли баги. Часто эти тесты задаются тестовыми сценариями, чтобы гарантировать, что тестировщики всё последовательно проверят.
Очевидно, что тестирование всех изменений вручную занимает много времени, оно однообразное и утомительное. Однообразие скучно, а скука приводит к ошибкам.
К счастью, есть прекрасный инструмент для однообразных задач: автоматизация.
Автоматизация однообразных тестов изменит вашу жизнь как разработчика. Автоматизируйте тесты, и вам больше не придётся бездумно следовать клик-протоколам, проверяя корректность работы программы. Автоматизируйте тесты, и вы не моргнув глазом измените кодовую базу. Если вы когда-либо пробовали делать крупномасштабный рефакторинг без надлежащего набора тестов, я уверен, вы знаете, в какой ужас это может превратиться. Как вы узнаете, если случайно сделаете ошибку в процессе? Ну, придётся щёлкать вручную по всем тестовым случаям, как же ещё. Но будем честными: вам это действительно нравится? Как насчёт того, чтобы даже после крупномасштабных изменений любые баги проявляли себя в течение нескольких секунд, пока вы пьёте кофе? По-моему, это гораздо приятнее.
Пирамида тестов
Если серьёзно подходить к автоматическим тестам, то есть одна ключевая концепция: пирамида тестов. Её представил Майк Кон в своей книге «Scrum: гибкая разработка ПО» (Succeeding With Agile. Software Development Using Scrum). Это отличная визуальная метафора, наталкивающая на мысль о разных уровнях тестов. Она также показывает объём тестов на каждом уровне.
Рис. 2. Пирамида тестов
Оригинальная пирамида тестов Майка Кона состоит из трёх уровней (снизу вверх):
Тем не менее, из-за своей простоты суть тестовой пирамиды представляет хорошее эмпирическое правило, когда дело доходит до создания собственного набора тестов. Из этой пирамиды главное запомнить два принципа:
Не привязывайтесь слишком сильно к названиям отдельных уровней пирамиды тестов. На самом деле они могут ввести в заблуждение: термин «сервисный тест» трудно понять (сам Кон заметил, что многие разработчики полностью игнорируют этот уровень). В наше время фреймворков для одностраничных приложений вроде React, Angular, Ember.js и других становится очевидным, что тестам UI не место на вершине пирамиды — вы прекрасно можете протестировать UI во всех этих фреймворках.
Учитывая недостатки оригинальных названий в пирамиде, вполне нормально придумать другие имена для своих уровней тестов. Главное, чтобы они соответствовали вашему коду и терминологии, принятой в вашей команде.
Какие инструменты и библиотеки мы рассмотрим
Пример приложения
Я написал простой микросервис с тестами из разных уровней пирамиды.
Это пример типичного микросервиса. Он предоставляет интерфейс REST, общается с БД и извлекает информацию из стороннего сервиса REST. Он реализован на Spring Boot и должен быть понятен даже если вы никогда не работали со Spring Boot.
Обязательно проверьте код на Github. В файле readme инструкции для запуска приложения и автоматических тестов на вашем компьютере.
Функциональность
У приложения простая функциональность. Оно обеспечивает интерфейс REST с тремя конечными точками:
GET /hello
Возвращает «Hello World». Всегда.
GET /hello /
Ищет человека с указанной фамилией. Если человек известен, возвращает «Hello
GET /weather
Возвращает текущие погодные условия в Гамбурге, Германия.
Высокоуровневые структуры
На высоком уровне у системы следующая структура:
Рис. 3. Высокоуровневая структура микросервиса
Наш микросервис обеспечивает интерфейс REST по HTTP. Для некоторых конечных точек сервис получает информацию из БД. В других случаях обращается по HTTP к внешнему API для получения и отображения текущей погоды.
Внутренняя архитектура
Изнутри у Spring Service типичная архитектура для Spring:
Рис. 4. Внутренняя структура микросервиса
Взгляните на кодовую базу и познакомьтесь с внутренней структурой. Это полезно для следующего шага: тестирования приложения!
Юнит-тесты
Основа вашего набора тестов состоит из юнит-тестов (модульных тестов). Они проверяют, что отдельный юнит (тестируемый субъект) кодовой базы работает должным образом. Модульные тесты имеют максимально узкую область среди всех тестов в наборе тестов. Количество юнит-тестов в наборе значительно превышает количество любых других тестов.
Рис. 5. Обычно юнит-тест заменяет внешних пользователей тестовыми дублями
Что такое юнит?
Если вы спросите трёх разных людей, что означает «юнит» в контексте юнит-тестов, то вероятно получите четыре разных, слегка отличающихся ответа. В определённой степени это вопрос вашего собственного определения — и это нормально, что здесь нет общепринятого канонического ответа.
Если вы пишете на функциональном языке, то юнитом скорее всего будет отдельная функция. Ваши юнит-тесты вызовут функцию с различными параметрами и обеспечат возврат ожидаемых значений. В объектно-ориентированном языке юнит может варьироваться от отдельного метода до целого класса.
Общительные и одинокие тесты
Некоторые утверждают, что всех участников (например, вызываемые классы) тестируемого субъекта следует заменить на имитации (mocks) или заглушки (stubs), чтобы создать идеальную изоляцию, избежать побочных эффектов и сложной настройки теста. Другие утверждают, что на имитации и заглушки следует заменять только участников, которые замедляют тест или проявляют сильные побочные эффекты (например, классы с доступом к БД или сетевыми вызовами).
Иногда эти два вида юнит-тестов называют одинокими (solitary) в случае тотального применения имитаций и заглушек или общительными (sociable) в случае реальных коммуникаций с другими участниками (эти термины придумал Джей Филдс для книги «Эффективная работа с юнит-тестами»). Если у вас есть немного свободного времени, можете спуститься в кроличью нору и разобраться в преимуществах и недостатках разных точек зрения.
Но в итоге не имеет значения, какой тип тестов вы выберете. Что реально важно, так это их автоматизация. Лично я постоянно использую оба подхода. Если неудобно работать с реальными участниками, я буду обильно использовать имитации и заглушки. Если чувствую, что привлечение реального участника даёт больше уверенности в тесте, то заглушу только самые дальние части сервиса.
Имитации и заглушки
Имитации (mocks) и заглушки (stubs) — это два разных типа тестовых дублёров (вообще их больше). Многие используют термины взаимозаменяемо. Думаю, что лучше соблюдать точность и держать в уме конкретные свойства каждого из них. К объектам из продакшна тестовые дублёры создают реализацию для тестов.
Проще говоря, вы заменяете реальную вещь (например, класс, модуль или функцию) поддельной копией. Подделка выглядит и действует как оригинал (даёт такие же ответы на те же вызовы методов), но это заранее установленные ответы, которые вы сами определяете для юнит-теста.
Тестовые дублёры используются не только в юнит-тестах. Более сложные дублёры применяются для контролируемой имитации целых частей вашей системы. Однако в юнит-тестах используется особенно много имитаций и заглушек (в зависимости от того, предпочитаете вы общительные или одиночные тесты) просто потому что множество современных языков и библиотек позволяют легко и удобно их создавать.
Независимо от выбранной технологии, в стандартной библиотеке вашего языка или какой-то популярной сторонней библиотеке уже есть элегантный способ настройки имитаций. И даже для написания собственных имитаций с нуля достаточно всего лишь написать поддельный класс/модуль/функцию с той же подписью, что и реальная, и настройки имитации для теста.
Ваши юнит-тесты будут работать очень быстро. На приличной машине можно прогнать тысячи модульных тестов за нескольких минут. Тестируйте изолированно небольшие фрагменты кодовой базы и избегайте контактов с БД, файловой системой и HTTP-запросов (ставя здесь имитации и заглушки), чтобы сохранить высокую скорость.
Поняв основы, со временем вы начнёте всё более свободно и легко писать юнит-тесты. Заглушка внешних участников, настройка входных данных, вызов тестируемого субъекта — и проверка, что возвращаемое значение соответствует ожидаемому. Посмотрите на разработку через тестирование (TDD), и пусть юнит-тесты направляют вашу разработку; если они применяются правильно, это поможет попасть в мощный поток и создать хорошую поддерживаемую архитектуру, автоматически выдавая всеобъемлющий и полностью автоматизированный набор тестов. Но это не универсальное решение. Попробуйте и посмотрите сами, подходит ли TDD в вашем конкретном случае.
Что тестировать?
Хорошо, что юнит-тесты можно писать для всех классов кода продакшна, независимо от их функциональности или того, к какому уровню внутренней структуры они принадлежат. Юнит-тесты подходят для контроллеров, репозиториев, классов предметной области или программ считывания файлов. Просто придерживайтесь практического правила один тестовый класс на один класс продакшна.
Юнит-тест должен как минимум протестировать открытый интерфейс класса. закрытые методы всё равно нельзя протестировать, потому что их нельзя вызвать из другого тестового класса. Защищённые или доступные лишь в пределах пакета (package-private) методы доступны из тестового класса (учитывая, что структура пакета тестового класса такая же, как на продакшне), но тестирование этих методов может уже зайти слишком далеко.
Когда дело доходит до написания юнит-тестов, есть тонкая черта: они должны гарантировать, что проверены все нетривиальные пути кода, включая дефолтный сценарий и пограничные ситуации. В то же время они не должны быть слишком тесно привязаны к реализации.
Тесты, слишком привязанные к коду продакшна, быстро начинают раздражать. Как только вы осуществляете рефакторинг кода (то есть изменяете внутреннюю структуру кода без изменения внешнего поведения), модульные тесты сразу ломаются.
Таким образом, вы теряете важное преимущество юнит-тестов: действовать в качестве системы безопасности для изменений кода. Вы скорее устанете от этих глупых тестов, которые падают каждый раз после рефакторинга, принося больше проблем, чем пользы; чья вообще была эта дурацкая идея внедрить тесты?
Чем же делать? Не отражайте в модульных тестах внутреннюю структуру кода. Тестируйте наблюдаемое поведение. Например:
если я введу значения x и y, будет ли результат z?
если я введу x и y, то обратится ли метод сначала к классу А, затем к классу Б, а затем сложит результаты от класса А и класса Б?
Как правило, закрытые методы следует рассматривать как деталь реализации. Вот почему даже не должно появляться желание их проверить.
Часто я слышу от противников модульного тестирования (или TDD), что написание юнит-тестов становится бессмысленным, если нужно проверить все методы для большого охвата тестирования. Они часто ссылаются на сценарии, где чрезмерно нетерпеливый тимлид заставил писать модульные тесты для геттеров и сеттеров и прочего тривиального кода, чтобы выйти на 100% тестового покрытия.
Это совершенно неправильно.
Да, вы должны протестировать публичный интерфейс. Но ещё более важно не тестировать тривиальный код. Не волнуйтесь, Кент Бек это одобряет. Вы ничего не получите от тестирования простых геттеров или сеттеров или других тривиальных реализаций (например, без какой-либо условной логики). И вы сэкономите время, так что сможете посидеть ещё на одном совещании, ура!
Но мне очень нужно проверить этот закрытый метод
Если вы когда-нибудь окажетесь в ситуации, когда вам очень-очень нужно проверить закрытый метод, нужно сделать шаг назад и спросить себя: почему?
Уверен, что здесь скорее проблема дизайна. Скорее всего, вы чувствуете необходимость протестировать закрытый метод, потому что он сложный, а тестирование метода через открытый интерфейс класса требует слишком неудобной настройки.
Всякий раз, когда я оказываюсь в такой ситуации, я обычно прихожу к выводу, что тестируемый класс переусложнён. Он делает слишком много и нарушает принцип единой ответственности — один из пяти принципов SOLID.
Для меня часто работает решение разделить исходный класс на два класса. Часто после минуты-другой размышлений находится хороший способ разбить большой класс на два меньших с индивидуальной ответственностью. Я перемещаю закрытый метод (который срочно надо протестировать) в новый класс и позволяю старому классу вызвать новый метод. Вуаля, неудобный для тестирования закрытый метод теперь публичен и легко тестируется. Кроме того, я улучшил структуру кода, внедрив принцип единой ответственности.
Cтруктура теста
Хорошая структура всех ваших тестов (не только модульных) такова:
Этот шаблон можно применить и к другим, более высокоуровневым тестам. В каждом случае они гарантируют, что тесты остаются лёгкими и читаемыми. Кроме того, написанные с учётом этой структуры тесты обычно короче и выразительнее.
Реализация юнит-теста
Теперь мы знаем, что именно тестировать и как структурировать юнит-тесты. Пришло время посмотреть на реальный пример.
Юнит-тест для метода hello(lastname) может выглядеть таким образом:
Мы пишем юнит-тесты в JUnit, стандартном фреймворке тестирования Java. Используем Mockito для замены реального класса PersonRepository на класс с заглушкой для теста. Эта заглушка позволяет указать предустановленные ответы, которые вернёт метод-заглушка. Подобный подход делает тест более простым и предсказуемым, позволяя легко настроить проверку данных.
Следуя структуре «трёх А» пишем два юнит-теста для положительного и отрицательного случаев, когда искомое лицо не может быть найдено. Положительный тестовый случай создаёт новый объект person и сообщает имитации репозитория возвращать этот объект, когда параметр lastName вызывается со значением Pan. Затем тест вызывает тестируемый метод. Наконец, он сравнивает ответ с ожидаемым.
Второй тест работает аналогично, но тестирует сценарий, в котором тестируемый метод не находит объект person для данного параметра.
Специализированные тестовые хелперы
Замечательно, что вы можете писать юнит-тесты для всей кодовой базы независимо от уровня архитектуры вашего приложения. Пример ниже показывает простой юнит-тест для контроллера. К сожалению, когда дело доходит до контроллеров Spring, у этого подхода есть недостаток: контроллер Spring MVC интенсивно использует аннотации с объявлениями прослушиваемых путей, используемых команд HTTP, параметров парсинга URL, параметров запросов и так далее. Простой вызов метода контроллера в юнит-тесте не проверит все эти важные вещи. К счастью, сообщество Spring придумало хороший тестовый хелпер, который можно использовать для улучшенного тестирования контроллера. Обязательно посмотрите MockMVC. Это даст отличный DSL для генерации поддельных запросов к контроллеру и проверки, что всё работает отлично. Я включил пример в код. Во многих фреймворках есть тестовые хелперы для упрощения тестов конкретных частей кода. Ознакомьтесь с документацией по своему фреймворку и посмотрите, предлагает ли там какие-либо полезные хелперы для ваших автоматизированных тестов.
Интеграционные тесты
Все нетривиальные приложения интегрированы с некоторыми другими частями (базы данных, файловые системы, сетевые вызовы к другим приложениям). В юнит-тестах вы обычно имитируете их для лучшей изоляции и повышения скорости. Тем не менее, ваше приложение будет реально взаимодействовать с другими частями — и это следует протестировать. Для этого предназначены интеграционные тесты. Они проверяют интеграцию приложения со всеми компонентами вне приложения.
Для автоматизированных тестов это означает, что нужно запустить не только собственное приложение, но и интегрируемый компонент. Если вы тестируете интеграцию с БД, то при выполнении тестов надо запустить БД. Чтобы проверить чтение файлов с диска нужно сохранить файл на диск и загрузить его в интеграционный тест.
Я ранее упоминал, что юнит-тесты — неопределённый термин. Ещё в большей степени это относится к интеграционным тестам. Для кого-то «интеграция» означает тестирование всего стека вашего приложения в комплексе с другими. Мне нравится более узкое определение и тестирование каждой точки интеграции по отдельности, заменяя остальные сервисы и базы данных тестовыми дублёрами. Вместе с контрактным тестированием и выполнением контрактных тестов на дублёрах и реальных реализациях можно придумать интеграционные тесты, которые быстрее, более независимы и обычно проще в понимании.
Узкие интеграционные тесты живут на границе вашего сервиса. Концептуально они всегда запускают действие, которое приводит к интеграции с внешней частью (файловой системой, базой данных, отдельным сервисом). Тест интеграции БД выглядит следующим образом:
Рис. 6. Тест на интеграцию БД интегрирует ваш код с реальной базой данных
Рис. 7. Этот вид интеграционного теста проверяет, что приложение способно правильно взаимодействовать с отдельными службами
Напишите интеграционные тесты для всех фрагментов кода, где выполняется сериализация или десериализация данных. Это происходит чаще, чем вы думаете. Подумайте о следующем:
При написании узких интеграционных тестов стремитесь локально запускать внешние зависимости: локальную базу данных MySQL, тест на локальной файловой системе ext4. Если интегрируетесь с отдельной службой, то или запустите экземпляр этой службы локально, или создайте и запустите поддельную версию, которая имитирует поведение реальной службы.
Если нет возможности локально запустить стороннюю службу, то лучше запустить выделенный тестовый инстанс и указать на него в интеграционным тесте. В автоматизированных тестах избегайте интеграции с реальной системой продакшна. Запуск тысяч тестовых запросов на систему продакшна — верный способ разозлить людей, потому что вы забиваете их логи (в лучшем случае) или просто ддосите их сервис (в худшем случае). Интеграция с сервисом по сети — типичное свойство широкого интеграционного теста. Обычно из-за неё тесты труднее писать и они медленнее работают.
Что касается пирамиды тестов, то интеграционные тесты находятся на более высоком уровне, чем модульные. Интеграция файловых систем и БД обычно гораздо медленнее, чем выполнение юнит-тестов с их имитациями. Их также труднее писать, чем маленькие изолированные модульные тесты. В конце концов, нужно думать о работе внешней части теста. Тем не менее, они имеют преимущество, потому что дают уверенность в правильной работе приложения со всеми внешними частями, с какими нужно. Юнит-тесты тут бесполезны.
Интеграция БД
PersonRepository — единственный класс репозитория во всей кодовой базе. Он опирается на Spring Data и не имеет фактической реализации. Он просто расширяет интерфейс CrudRepository и предоставляет единственный заголовок метода. Остальное — магия Spring.
Чтобы облегчить выполнение тестов на вашем компьютере (без установки базы данных PostgreSQL), наш тест подключается к базе данных в памяти H2.
Понимаю, что здесь нужно знать и понимать кучу особенностей Spring. Придётся перелопатить кучу документации. Финальный код простой с виду, но его трудно понять, если вы не знаете конкретных особенностей Spring.
Кроме того, работа с базой данных в памяти — рискованное дело. В конце концов, наши интеграционные тесты работают с БД другого типа, чем в продакшне. Попробуйте и решите сами, предпочесть ли магию Spring и простой код — или явную, но более подробную реализацию.
Ну, хватит объяснений. Вот простой интеграционный тест, который сохраняет объект Person в базу данных и находит его по фамилии.
Как видите, наш интеграционный тест следует той же структуре «трёх А», что и юнит-тесты. Говорил же, что это универсальная концепция!
Интеграция с отдельными сервисами
Наш микросервис получает погодные данные с darksky.net через REST API. Конечно, мы хотим убедиться, что сервис правильно отправляет запросы и разбирает ответы.
При выполнении автоматических тестов желательно избежать взаимодействия с настоящими серверами darksky. Лимиты на нашем бесплатном тарифе — лишь одна из причин. Главное — это отвязка. Наши тесты должны запускаться независимо от того, какие справляются со своей работой милые люди в darksky.net. Даже если наша машина не может достучаться до серверов darksky или они закрылись на обслуживание.
Чтобы избежать взаимодействия с реальными серверами darksky, мы для интеграционных тестов запускаем собственный, поддельный сервер darksky. Это может показаться очень трудной задачей. Но она упрощается благодаря таким инструментам, как Wiremock. Смотрите сами:
Для Wiremock создаем инстанс WireMockRule на фиксированном порту ( 8089 ). С помощью DSL можно настроить сервер Wiremock, определить конечные точки для прослушивания и предустановленные ответы.
Далее вызываем тестируемый метод — тот, который обращается к сторонней службе — и проверяем, что результат правильно парсится.
Обратите внимание, что определённый здесь порт должен быть тем же, что мы указали при создании инстанса WireMockRule для теста. Замена URL-адреса реального API на поддельный стала возможной благодаря введению URL-адреса в конструктор класса WeatherClient :
С инструментами вроде Wiremock написание узких интеграционных тестов для отдельного сервиса становится достаточно простой задачей. К сожалению, у такого подхода есть недостаток: как гарантировать, что созданный нами поддельный сервер ведёт себя как настоящий? При текущей реализации отдельный сервис может изменить свой API, и наши тесты всё равно пройдут как ни в чём ни бывало. Сейчас мы просто тестируем, что WeatherClient способен воспринимать ответы от поддельного сервера. Это начало, но оно очень хрупкое. Проблему решают сквозные тесты и тестирование на реальном сервисе, но так мы становимся зависимы от его доступности. К счастью, есть лучшее решение этой дилеммы — контрактные тесты с участием и имитации, и реального сервера гарантируют, что имитация в наших интеграционных тестах точно соответствует оригиналу. Посмотрим, как это работает.
Контрактные тесты
Более современные компании нашли способ масштабирования разработки путём распределения работ среди разных команд. Они создают отдельные, слабо связанные службы, не мешая друг на другу, и интегрируют их в большую, цельную систему. Именно с этим связана недавняя шумиха вокруг микросервисов.
Разделение системы на множество небольших сервисов часто означает, что эти сервисы должны взаимодействовать друг с другом через определённые (желательно чётко определенные, но иногда случайно созданные) интерфейсы.
Интерфейсы между разными приложения могут быть реализованы в разных форматах и технологиях. Самые распространённые:
Рис. 8. Каждый интерфейс задействует поставщика (или издателя) и потребителя (или подписчика). Спецификацией интерфейса можно считать контракт.
Поскольку сервисы поставщика и потребителя распределяются по разным командам, то вы оказываетесь в ситуации, когда нужно чётко указать интерфейс между ними (так называемый контракт). Традиционно компании подходят к этой проблеме следующим образом:
В более гибкой организации следует выбрать более эффективный и менее расточительный маршрут. Приложение создаётся в рамках одной организации. Не должно быть проблемой переговорить с разработчиками других сервисов вместо того, чтобы забрасывать им чрезмерно подробную готовую документацию. В конце концов, это ваши сотрудники, а не сторонний вендор, с которым можно общаться только через службу поддержки клиентов или пуленепробиваемые юридические контракты.
Ориентированные на пользователя контрактные тесты (CDC-тесты) позволяют потребителям управлять реализацией контракта. С помощью CDC потребители пишут тесты, которые проверяют интерфейс для всех данных, которые им нужны. Затем команда публикует эти тесты, чтобы разработчики службы поставщика могли легко получить и запустить эти тесты. Теперь они могут разработать свой API, запустив тесты CDC. После прогона всех тестов они знают, что удовлетворили все потребности команды на стороне потребителя.
Рис. 9. Контрактные тесты гарантируют, что поставщик и все потребители интерфейса придерживаются определённого контракта интерфейса. С помощью CDC-тестов потребители интерфейса публикуют свои требования в виде автоматизированных тестов; поставщики непрерывно получают и выполняют эти тесты
В последние годы подход CDC становится более популярным и создано несколько инструментов для упрощения написания и обмена тестами.
Pact — разумный выбор, чтобы начать работу с CDC. Документация сначала ошеломляет, но если набраться терпения, то её можно одолеть. Она помогает получить твёрдое понимание CDC, что в свою очередь облегчает вам задачу пропагандировать CDC для работы с другими командами.
Ориентированные на пользователя контрактные тесты могут кардинально упростить работу автономных команд, которые начнут действовать быстро и уверенно. Сделайте одолжение, ознакомьтесь и попробуйте эту концепцию. Качественный набор CDC-тестов неоценим, чтобы быстро продолжать разработку, не ломая прочие сервисы и не огорчая другие команды.
Тест потребителя (наша команда)
Наш микросервис использует погодный API. Так что наша обязанность — написать тест потребителя, который определяет наши ожидания по контракту (API) между нашим микросервисом и погодной службой.
Сначала включаем библиотеку для написания тестов потребителя в нашу build.gradle :
Благодаря этой библиотеке мы можем реализовать тест потребителя и использовать сервисы имитации Pact:
Как можно понять, именно отсюда взялась часть «ориентирование на потребителя» в определении CDC. Потребитель управляет реализацией интерфейса, описывая свои ожидания. Поставщик должен убедиться, что он выполняет все ожидания. Никаких лишних спецификаций, YAGNI и все дела.
Передача pact-файла в команду поставщика может пройти несколькими путями. Простой — зарегистрировать его в системе управления версиями и сказать команде поставщика всегда брать последнюю версию файла. Более продвинутый способ — использовать репозиторий артефактов вроде Amazon S3 или Pact Broker. Начинайте с простого и растите по мере необходимости.
В реальном приложении вам не нужны одновременно и интеграционный тест, и тест потребителя для клиентского класса. Наш пример кода содержит оба только для демонстрации, как использовать каждый из них. Если вы хотите написать CDC-тесты на Pact, я рекомендую оставаться с ним. В этом случае преимущество в том, что вы автоматически получаете pact-файл с ожиданиями от контракта, которые другие команды могут использовать, чтобы легко сделать свои тесты поставщика. Конечно, это имеет смысл только если вы сможете убедить другую команду использовать Pact. Если нет, то используйте интеграционный тест интеграции в сочетании с Wiremock как достойную альтернативу.
Тест поставщика (другая команда)
Тесты поставщика должны реализовать те, кто предоставляет погодный API. Мы используем публичный API от darksky.net. Теоретически, команда darksky со своей стороны должна выполнить тест поставщика и убедиться, что не нарушает контракт между своим приложением и нашим сервисом.
Очевидно, что их не волнует наше скромное тестовое приложение — и они не будут делать для нас CDC-тест. Есть большая разница между публичным API и организацией, использующей микросервисы. Общедоступный API не может учитывать нужды каждого отдельного потребителя, иначе не сможет нормально работать. Внутри своей организации вы можете и должны их учитывать. Скорее всего, ваше приложение будет обслуживать несколько, ну может пару десятков потребителей. Ничего не мешает написать тесты поставщика для этих интерфейсов, чтобы сохранить стабильную систему.
Команда поставщика получает pact-файл и запускает его на своём сервисе. Для этого она реализует тест, который считывает pact-файл, ставит несколько заглушек и проверяет на своём сервисе ожидания, определённые в pact-файле.
Сообщество проекта Pact написало несколько библиотек для реализации тестов поставщика. В их основном репозитории GitHub неплохой выбор библиотек для потребителей и провайдеров. Выберите ту, которая лучше всего соответствует вашему стеку технологий.
Для простоты предположим, что API darksky тоже реализован в Spring Boot. В этом случае они могут использовать библиотеку Pact Spring, которая хорошо подключается к механизмам MockMVC Spring. Гипотетический тест поставщика, который могла бы реализовать команда darksky.net, выглядит так:
Как видите, от поставщика требуется лишь загрузить pact-файл (например, @PactFolder определяет, откуда загружать полученные pact-файлы), а затем определить, как обеспечить тестовые данные для предопределённых состояний (например, с помощью имитаций Mockito). Не нужно писать какой-то специальный тест. Всё берётся из pact-файла. Важно, чтобы тест поставщика соответствовал имени поставщика и состоянию, объявленным в тесте потребителя.
Тест поставщика (наша команда)
Мы посмотрели, как тестировать контракт между нашим сервисом и поставщиком погодной информации. В этом интерфейсе наш сервис выступает в качестве потребителя, а метеорологическая служба — в качестве поставщика. Подумав ещё, мы увидим, что наш сервис тоже выступает в роли поставщика для других: мы предоставляем REST API с несколькими конечными точками для других потребителей.
Поскольку мы знаем важность контрактных тестов, то конечно напишем тест и для этого контракта. К счастью, наши контракты ориентированы на потребителя, так что все команды-потребители присылают нам свои pact-файлы, которые мы можем использовать для реализации тестов поставщика для нашего REST API.
Сначала добавим в наш проект библиотеку поставщика Pact для Spring:
Реализация теста поставщика следует той же описанной схеме. Для простоты я зарегистрирую pact-файл от нашего простого потребителя в репозитории нашего сервиса. Для нашего случая так проще, а в реальной жизни, вероятно, придётся использовать более сложный механизм для распространения pact-файлов.
Показанный ExampleProviderTest должен предоставить состояние в соответствии с полученным pact-файлом, вот и всё. Когда мы запустим тест, Pact подберёт pact-файл и отправит HTTP-запрос к нашему сервису, который ответит в соответствии с заданным состоянием.
Тесты UI
У большинства приложений есть какой-то пользовательский интерфейс. Обычно мы говорим о веб-интерфейсе в контексте веб-приложений. Люди часто забывают, что REST API или интерфейс командной строки — это такой же UI, как и причудливый веб-интерфейс.
Тесты UI проверяют правильность работы пользовательского интерфейса приложения. Действия пользователя должны инициировать правильные события, данные должны представляться пользователю, состояние UI должно изменяться ожидаемым образом.
Иногда говорят, что тесты UI и сквозные тесты — это одно и то же (как говорит Майк Кон). Для меня это отождествление двух вещей с весьма ортогональными концепциями.
Да, тестирование приложения от начала до конца часто означает прохождение через пользовательский интерфейс. Но обратное неверно.
Тестирование пользовательского интерфейса необязательно должно проводиться в сквозном режиме. В зависимости от используемой технологии, тестирование UI может оказаться таким же простым, как написание некоторых модульных тестов для фронтенда JavaScript с заглушенным бэкендом.
Для тестирования UI традиционных веб-приложений предназначены специальные инструменты вроде Selenium. Если вы считаете пользовательским интерфейсом REST API, то достаточно правильных интеграционных тестов вокруг API.
В веб-интерфейсах желательно проверить несколько аспектов UI, в том числе поведение, вёрстка, юзабилити, соблюдение фирменного стиля и др.
К счастью, тестирование поведения UI довольно простое. Щёлкаете здесь, вводите данные там — и проверяете, что состояние UI меняется соответствующим образом. Современные фреймворки для одностраничных приложений (react, vue.js, Angular и прочие) часто поставляются с инструментами и хелперами для тщательного тестирования этих взаимодействий на довольно низком уровне (в юнит-тесте). Даже если выкатить собственную реализацию фронтенда на ванильном JavaScript, всё равно можно использовать обычные инструменты тестирования, такие как Jasmine и Mocha. Для более традиционного приложения с рендерингом на стороне сервера наилучшим выбором станут тесты на основе Selenium.
Цельность вёрстки веб-приложения проверить немного сложнее. В зависимости от приложения и потребностей пользователей может возникнуть необходимость убедиться, что изменения кода случайно не нарушают вёрстку сайта.
Но компьютеры плохо справляются с проверкой, что всё «нормально выглядит» (возможно, в будущем какой-то умный алгоритм машинного обучения изменит это).
Есть некоторые инструменты, чтобы попробовать автоматическую проверку дизайна веб-приложения в конвейере сборки. Большинство из них используют Selenium для открытия веб-приложения в разных браузерах и форматах, произведения скриншотов и сравнения с ранее сделанными скриншотами. Если старый и новый скриншоты отличаются неожиданным образом, то инструмент подаст сигнал.
Один из таких инструментов — Galen. Некоторые команды используют lineup и его брата jlineup на основе Java для достижения аналогичного результата. Оба инструмента применяют тот же подход на основе Selenium.
Как только вы хотите проверить удобство использования и приятный дизайн — вы покидаете пространство автоматизированного тестирования. Здесь придётся полагаться на исследовательские тесты, тесты юзабилити (вплоть до простейших холл-тестов на случайных людях). Придётся проводить демонстрации пользователям и проверять, нравится ли им продукт и могут ли они использовать все функции без разочарования или раздражения.
Сквозные тесты
Тестирование развёрнутого приложения через UI — это самый полный тест, какой только можно провести. Описанные выше тесты UI через WebDriver — хорошие примеры сквозных тестов.
Рис. 11. Сквозные тесты проверяют полностью интегрированную систему целиком
Сквозные тесты (также называемые тестами широкого стека) дают максимальную уверенность, работает программное обеспечение или нет. Selenium и протокол WebDriver позволяют автоматизировать тесты, автоматически отправляя headless-браузер на развёрнутые сервисы для выполнения кликов, ввода данных и проверки состояния UI. Можно использовать Selenium напрямую или применить инструменты на его основе, такие как Nightwatch.
У сквозных тесты другие проблемы. Они известны своей ненадёжностью, сбоями по неожиданным и непредвиденным причинам. Довольно часто это ложноположительные сбои. Чем более сложный UI, тем более хрупкими становятся тесты. Причуды браузера, проблемы с синхронизацией, анимация и неожиданные всплывающие диалоги — лишь некоторые из причин, из-за которых я потратил больше времени на отладку, чем хотелось бы.
В мире микросервисов также непонятно, кто отвечает за написание этих тестов. Поскольку они охватывают несколько сервисов (всю систему), то нет одной конкретной команды, ответственной за написание сквозных тестов.
Если есть централизованная команда обеспечения качества, они выглядят хорошим кандидатом. Опять же, заводить централизованную команду QA строго не рекомендуется, такого не должно быть в мире DevOps, где все команды по-настоящему универсальны. Нет простого ответа, кто должен владеть сквозными тестами. Может, в вашей организации есть какая-то инициативная группа или гильдия качества, чтобы позаботиться о них. Здесь многое зависит от конкретной организации.
Кроме того, сквозные тесты требуют серьёзной поддержки и выполняются довольно медленно. Если у вас много микросервисов, то вы даже не сможете запускать сквозные тесты локально, потому что тогда понадобится и все микросервисы запускать локально. Попробуйте запустить сотни приложений на своём компьютере, тут никакой оперативки не хватит.
Из-за высоких расходов на обслуживание следует свести число сквозных тестов к абсолютному минимуму.
Подумайте о самых главных взаимодействиях пользователей с приложением. Придумайте главные «маршруты» пользователей от экрана к экрану, чтобы автоматизировать самые важные из этих шагов в сквозных тестах.
Если вы делаете интернет-магазин, то самым ценным «маршрутом» будет поиск продукта — помещение его в корзину — оформление заказа. Вот и всё. Пока этот маршрут работает, нет особых проблем. Возможно, вы найдёте еще пару важных маршрутов для сквозных тестов. Всё остальное, вероятно, принесёт больше проблем, чем пользы.
Помните: в вашей пирамиде тестов много низкоуровневых тестов, где мы уже протестировали все варианты пограничных ситуаций и интеграции с другими частями системы. Нет необходимости повторять эти тесты на более высоком уровне. Большие усилия по техническому обслуживанию и много ложных срабатываний слишком замедлит вашу работу, а рано или поздно лишит вас доверия к тестам вообще.
Сквозные тесты UI
Для сквозных тестов многие разработчики выбирают Selenium и протокол WebDriver. С Selenium можете выбрать любой браузер и натравить его на сайт. Пусть нажимает повсюду кнопки и ссылки, вводит данные и проверяет изменения в UI.
Запуск полноценного браузера в тестовом наборе может стать проблемой. Особенно если сервер непрерывной доставки, где работает наш конвейер, не способен развернуть браузер с UI (например, потому что X-Server недоступен). В этом случае можно запустить виртуальный X-Server вроде xvfb.
Более новый подход заключается в использовании headless-браузера (т.е. браузера без пользовательского интерфейса) для тестов WebDriver. До недавнего времени чаще всего для автоматизации браузерных задач использовался PhantomJS. Но когда Chromium и Firefox внедрили headless-режим из коробки, PhantomJS внезапно устарел. В конце концов, лучше протестировать сайт с помощью реального браузера, который действительно есть у пользователей (например, Firefox и Chrome), а не с помощью искусственного браузера только потому что это удобно вам как разработчику.
Оба headless-браузера Firefox и Chrome совершенно новые и ещё не получили широкого распространения для тестов WebDriver. Мы не хотим ничего усложнять. Вместо возни со свеженькими headless-режимами давайте придерживаться классического способа, то есть Selenium в связке с обычным браузером. Вот как выглядит простой сквозной тест, который запускает Chrome, переходит к нашему сервису и проверяет содержимое сайта:
Обратите внимание, что этот тест будет работать, только если Chrome установлен на машине, где запускается тест (ваш локальный компьютер, сервер CI).
Сквозной тест REST API
Для повышения надёжности тестов хорошая идея — избегать GUI. Такие тесты более стабильны, чем полноценные сквозные тесты, и в то же время покрывают значительную часть стека приложения. Это может пригодиться, если тестировать приложение через веб-интерфейс особенно сложно. Может, у вас даже нет веб-интерфейса, а только REST API (потому что одностраничное приложение где-то общается с этим API или просто потому что вы презираете всё красивое и блестящее). В любом случае ситуация подходит для подкожного теста (subcutaneous test), который тестирует всё, что находится под GUI. Если вы обслуживаете REST API, то такой тест будет правильным, как в нашем примере:
Позвольте показать ещё одну библиотеку, которая пригодится при тестировании сервиса, предоставляющего REST API. Библиотека REST-assured даёт хороший DSL для запуска реальных HTTP-запросов к API и оценки полученных ответов.
С помощью этой библиотеки можно реализовать сквозной тест для нашего REST API:
Приёмочные тесты — ваши фичи правильно работают?
Чем выше вы поднимаетесь в пирамиде тестов, тем больше вероятность возникновения вопросов: правильно ли работают фичи с точки зрения пользователя. Вы можете рассматривать приложение как чёрный ящик и изменить направленность тестов от прошлого:
когда я ввожу x и y, возвращаемое значение должно быть z
учитывая, что есть авторизованный пользователь
и есть изделие «велосипед»
когда пользователь переходит на страницу описания изделия «велосипед»
и нажимает кнопку «Добавить в корзину»
тогда изделие «велосипед» должно быть в его корзине
Бывает, что такие тесты называют функциональными или приёмочными. Некоторые говорят, что функциональные и приёмочные тесты — это разные вещи. Иногда термины объединяют. Иногда люди бесконечно спорят о формулировках и определениях. Часто такие обсуждения вводят ещё бóльшую путаницу.
Вот что: в какой-то момент следует убедиться, что ваша программа правильно работает с точки зрения пользователя, а не только с технической точки зрения. Как вы называете эти тесты — на самом деле не так важно. А вот наличие этих тестов важно. Выберите любой термин, придерживайтесь его и напишите эти тесты.
Можно ещё упомянуть BDD (разработка, основанная на описании поведения) и инструменты для нее. BDD и соответствующий стиль написания тестов — хороший трюк, чтобы изменить своё мышление от деталей реализации к потребностям пользователей. Не бойтесь и попробуйте.
Приёмочные тесты могут проводиться на разных уровнях детализации. В основном они будут достаточно высокого уровня и тестировать сервис через UI. Но важно понимать, что технически нет обязательного требования писать приёмочные тесты именно на самом высоком уровне пирамиды тестов. Если структура приложения и имеющийся сценарий позволяют написать приёмочный тест на более низком уровне, сделайте это. Тест низкого уровня лучше, чем высокого. Концепция приёмочных тестов — доказать, что фичи приложения правильно работают для пользователя — полностью ортогональна вашей пирамиде тестов.
Исследовательское тестирование
Даже самые прилежные усилия по автоматизации тестов не идеальны. Иногда в автоматических тестах вы пропускаете определённые пограничные случаи. Иногда просто невозможно обнаружить определённую ошибку, написав модульный тест. Некоторые проблемы качества вообще не проявятся в автоматизированных тестах (подумайте о дизайне или юзабилити). Несмотря на самые лучшие намерения в отношении автоматизации тестов, ручные тесты в некоторых отношениях по-прежнему незаменимы.
Рис. 12. Исследовательское тестирование выявит проблемы качества, незамеченные в процессе сборки
Включите исследовательские тесты в свой набор тестов. Эта процедура тестирования вручную подчёркивает свободу и творческие способности тестировщика, который способен найти проблемы качества в работающей системе. Просто выделите немного времени в расписании, засучите рукава и постарайтесь вызвать сбой приложения каким-нибудь способом. Включите деструктивное мышление и придумывайте способы, как спровоцировать проблемы и ошибки в программе. Документируйте всё, что найдёте. Ищите баги, проблемы дизайна, медленное время отклика, отсутствующие или вводящие в заблуждение сообщения об ошибках и всё остальное, что вас раздражает как пользователя.
Хорошая новость в том, что вы легко можете автоматизировать тесты для большинства найденных ошибок. Написание автоматизированных тестов для найденных ошибок гарантирует, что в будущем не будет регрессий этой ошибки. Кроме того, это помогает выяснить корневую причину проблемы при исправлении бага.
Во время исследовательского тестирования вы обнаружите проблемы, которые незаметно проскользнули через конвейер сборки. Не расстраивайтесь. Это хорошая обратная связь для улучшения конвейера сборки. Как и с любой обратной связью, обязательно отреагируйте со своей стороны: подумайте, какие действия предпринять, чтобы избежать такого рода проблем в будущем. Может, вы пропустили определённый набор автоматических тестов. Возможно, были небрежны с автоматизированными тестами на этом этапе и следует более тщательно проводить тесты в будущем. Возможно, есть какой-то блестящий новый инструмент или подход, который можно использовать в своем конвейере, чтобы избежать таких проблем. Обязательно отреагируйте так, чтобы ваш конвейер и вся система поставки программного обеспечения стали лучше и совершенствовались с каждым шагом.
Путаница с терминологией в тестировании
Всегда сложно говорить о разных классификациях тестов. Моё понимание юнит-тестов (модульных тестов) может слегка отличаться от вашего. С интеграционными тестами ещё хуже. Для некоторых людей интеграционное тестирование — это очень широкая деятельность, которая тестирует множество различных частей всей системы. Для меня это довольно узкая вещь: тестирование только интеграции с одной внешней частью за раз. Некоторые называют это интеграционными тестами, некоторые — компонентными, другие предпочитают термин сервисный тест. Кто-то заявит, что это вообще три совершенно разные вещи. Нет правильного или неправильного определения. Сообщество разработчиков ПО просто не установило чётко определённых терминов в тестировании.
Не зацикливайтесь на двусмысленных терминах. Не имеет значения, называете вы это сквозным тестом, тестом широкого стека или функциональным тестом. Неважно, если ваши интеграционные тесты означают для вас не то, что для людей в другой компании. Да, было бы очень хорошо, если бы наша отрасль могла чётко определить термины и все бы их придерживались. К сожалению, этого ещё не произошло. И поскольку в тестировании много нюансов, то всё равно мы имеем дело скорее с широким спектром тестов, чем с кучей дискретных множеств, что ещё больше усложняет чёткую терминологию.
Важно воспринимать это так: вы просто находите термины, которые работают для вас и вашей команды. Ясно определите для себя различные типы тестов, которые хотите написать. Согласуйте термины в своей команде и найдите консенсус относительно охвата каждого типа теста. Если в своей команде (или даже во всей организации) вы будете последовательны с этими терминами, то это всё, о чем нужно позаботиться. Саймон Стюарт хорошо подытожил это в подходе, который используется в Google. Думаю, это прекрасная демонстрация, что не стоит слишком зацикливаться на названиях и конвенциях по терминам.
Внедрение тестов в конвейер развёртывания
Если вы используете непрерывную интеграцию или непрерывную доставку, то ваш конвейер развёртывания запускает автоматические тесты каждый раз при внесении изменений в ПО. Обычно конвейер разделяется на несколько этапов, которые постепенно дают всё больше уверенности, что ваша программа готова к развёртыванию в рабочей среде. Услышав обо всех разновидностях тестов, возможно, вы задаётесь вопросом, как поместить их в конвейер развёртывания. Чтобы ответить на это, следует просто подумать об одной из самых основополагающих ценностей непрерывной доставки (это одна из ключевых ценностей экстремального программирования и гибкой разработки): о быстрой обратной связи.
Хороший конвейер сборки максимально быстро сообщает об ошибках. Вы не хотите ждать целый час, чтобы узнать, что последнее изменение сломало некоторые простые модульные тесты. Если конвейер работает так медленно, то вы могли уже уйти домой, когда поступила обратная связь. Информация должна приходить в течение нескольких секунд или нескольких минут с быстрых тестов на ранних этапах конвейера. И наоборот, более длительные тесты — обычно с более широкой областью — размещаются на более поздних этапах, чтобы не тормозить фидбек от быстрых тестов. Как видите, этапы конвейера развёртывания определяются не типами тестов, а их скоростью и областью действия. Поэтому очень разумно может быть разместить некоторые из самых узких и быстрых интеграционных тестов на ту же стадию, что и юнит-тесты — просто потому что они дают более быструю обратную связь. И необязательно проводить строгую линию по формальному типу тестов.
Избегайте дублирования тестов
Есть ещё одна ловушка, которую следует избегать: дублирование тестов на разных уровнях пирамиды. Чутьё говорит, что тестов много не бывает, но позвольте вас заверить: бывает. Каждый тест в тестовом наборе — дополнительный багаж, который не обходится бесплатно. Написание и ведение тестов требует времени. Чтение и понимание чужого теста требует времени. И конечно, выполнение тестов тоже требует времени.
Как и с производственным кодом, следует стремиться к простоте и избегать дублирования. В контексте реализации пирамиды тестов есть два эмпирических правила:
Сформулируем иначе: если тест более высокого уровня даёт больше уверенности, что приложение работает правильно, то нужно иметь такой тест. Написание модульного теста для класса контроллера помогает проверить логику внутри самого контроллера. Тем не менее, это не скажет вам, действительно ли конечная точка REST, которую предоставляет этот контроллер, отвечает на запросы HTTP. Таким образом, вы продвигаетесь вверх по пирамиде тестов и добавляете тест, который проверяет именно это, но не более того. В тесте более высокого уровня вы не тестируете всю условную логику и пограничные случаи, которые уже покрыты юнит-тестами более низкого уровня. Убедитесь, что тест высокого уровня фокусируется только на том, что не покрыто тестами более низкого уровня.
Я строго отношусь к исключению тестов, не имеющих ценности. Удаляю высокоуровневые тесты, которые уже покрыты на более низком уровне (учитывая, что они не дают дополнительной ценности). Заменяю тесты более высокого уровня тестами более низкого уровня, если это возможно. Иногда трудно удалить лишний тест, особенно если придумать его было непросто. Но вы рискуете создать невозвратные затраты, так что смело жмите Delete. Нет причин тратить драгоценное время на тест, который перестал приносить пользу.
Пишите чистый код для тестов
Как и в отношении обычного кода, следует позаботиться о хорошем и чистом коде тестов. Вот ещё несколько советов для создания поддерживаемого тестового кода, прежде чем начинать работу и создавать автоматизированный набор тестов:
Заключение
Вот и всё! Знаю, это было долгое и трудное объяснение, почему и как следует проводить тестирование. Отличная новость в том, что эта информация практически не имеет срока давности и не зависит от того, какую программу вы создаёте. Работаете вы над микросервисами, устройствами IoT, мобильными приложениями или веб-приложениями, уроки из этой статьи применимы ко всему.
Надеюсь, что в статье есть что-то полезное. Теперь вперёд, изучите пример кода и примените усвоенные понятия в своём наборе тестов. Создание крепкого набора тестов требует определённых усилий. Это окупится в долгосрочной перспективе и сделает вашу жизнь разработчика более спокойной, поверьте мне.