Не секрет, что тестирование обычно отстает от разработки по времени: пока у тебя нет готового решения, ты не можешь полноценно начать тестирование. Что уж говорить об автоматизации этого процесса. Но если качественно проработаны требования к создаваемой системе и правильно спроектирована архитектура автотестов, тестирование может идти параллельно с разработкой или обгонять ее. Более того, когда появится готовое решение, мы в кратчайшие сроки сможем перепроверить, насколько оно соответствует требованиям. Когда тесты пишутся до разработки кода, то такой подход называется TDD – Test Driven Development (разработка через тестирование). Стоит сказать, что на проекте в НСПК мы немного отошли от канонов: TDD предполагает, что тесты пишет разработчик на уровне модулей, у нас же этим занимался отдел автоматизации тестирования на уровне системы в целом. Для части функционала мы разработали тесты раньше, чем был готов код его реализующий.У TDD-подхода есть ряд преимуществ. Он позволяет находить дефекты на самых ранних этапах разработки. В то же время команда разработчиков заранее думает о том, чтобы решение было тестопригодно, соответственно, код становится более поддерживаемым.
Кроме того, тесты позволяют производить рефакторинг кода без риска его испортить: при внесении изменений в хорошо протестированный код риск появления новых ошибок значительно ниже. Если новая функциональность приводит к ошибкам, тесты сразу же это покажут.
Дополнительным плюсом является тот факт, что тесты могут использоваться в качестве документации (об этом мы скажем ниже). И наконец, несмотря на то что при разработке через тестирование требуется написать большее количество кода, общее время, затраченное на разработку, обычно оказывается меньше. Поскольку тесты защищают от ошибок, время, уходящее на отладку, многократно снижается. Рецепт быстрой разработки звучит так: большое количество тестов помогает уменьшить количество ошибок в коде.
При этом у TDD присутствуют слабые места. Подход не позволяет автоматически продемонстрировать адекватность разработанного кода в области безопасности данных и взаимодействия между процессами. В силу того, что UI очень часто меняется и поведение пользователя формализовать сложнее, чем протокол обмена данными, в этой области TDD практически неприменим – временные затраты на реализацию возможности его использования превысят общее время, отведенное на разработку.
Используемые инструменты
Для описания тестов мы использовали Cucumber – инфраструктуру тестирования, которая дает возможность устранить коммуникативный разрыв между членами команды (в нее могут входить тестировщики, аналитики, разработчики, представители бизнеса). Тесты разрабатываются в строго определенной нотации, о которой договариваются все участники, чтобы одинаково понимать процесс.
Поведением разработки при этом можно управлять в стиле Given/When/Then (условия/операция/результат). Контрольные тесты записываются в файлы функций, охватывающие один или несколько сценариев тестирования. Cucumber интерпретирует тесты на указанном языке программирования. В нашем случае тесты преобразуются в Java-код.
Конечно, это не что-то сверхновое, грамотные разработчики давно взяли на вооружение разделение кода в своих тестах в следующем виде и последовательности: подготовка к тесту, тестируемое действие, проверка результата выполнения этого действия. В нотации Given/When/Then эти этапы просто немного по-другому обозначены и подняты на уровень всей команды. Например, в Given описывается контекст выполнения сценария тестирования (как вариант, точки вызова сценария в приложении), а также содержатся любые необходимые для выполнения сценария данные. When говорит об операциях, результат или поведение которых мы тестируем. Then определяет ожидаемый результат. Пример, приведен на рис.1.
Рис. 1. Нотация Given/When/Then
Git (система хранения и управления версиями кода)
Вопрос организации хранения и доступа к коду в команде разработки уже давно не является сакральным знанием. Нет ни одного серьезного проекта, где бы весь код хранился локально у одного разработчика. Код должен быть доступен всем участникам проекта в любое время, независимо от других людей в команде. При этом было бы неплохо также иметь доступ к его ранним версиям. Более того, в больших командах есть практика веток, когда та или иная функциональность разрабатывается и тестируется в отдельной ветке, а затем сливается в основной ствол. В качестве системы хранения и управления версиями кода на проекте в НСПК мы использовали Git.
Code Review (инспекция кода)
Code Review (CR) – инженерная практика в терминах гибкой методологии разработки. Это анализ (инспекция) кода, позволяющая выявить ошибки, недочеты, расхождения в стиле написания кода, в соответствии написанного кода поставленной задаче. Команда разработки на проекте в НСПК у нас состояла из 7 разработчиков. Как только один разработчик заканчивал свой кусок функциональности, он отправлял запрос на изменение. Как только любой другой разработчик освобождался, он анализировал кусок кода коллеги на предмет единого оформления, правильной реализации логики и эффективности использования кода.
У такого подхода есть ряд преимуществ:
- Выявление ошибок на ранних этапах, что позволяет экономить время тестирования.
- Распространение знаний. Во многих командах у каждого программиста есть кусок кода в проекте, за который он ответственен. Он и только он сфокусирован на этом коде. Пока коллеги не поломают что-то, связанное с этим кодом, они не обращают на него внимания. Побочный эффект от этого состоит в том, что только один человек полностью понимает, как работает определенный компонент. Если этот человек не укладывается в сроки или заболел, мало кто сможет оперативно помочь ему и взять часть работы на себя. В случае с CR будут, как минимум, два человека, которые знакомы с кодом, – автор и reviewer. Reviewer не знает код так, как его знает автор, но он знаком со структурой и архитектурой кода, что очень важно.
- Единый стиль написания кода у всей команды.
При этом Code Review требует наличия у разработчиков соответствующего опыта, поэтому на первых этапах работы процесс может идти медленно.
Unit Tests
Разработчики не защищены от влияния человеческого фактора, поэтому следующим этапом после инспекции кода должно стать проведение модульных тестов, которые будут проверять разработанный код на соблюдение контрактов с другими модулями.
Идея состоит в том, чтобы писать тесты для каждой нетривиальной или критичной для бизнеса функциональности. Это позволяет достаточно быстро проверить, не привело ли очередное изменение кода к деградации системы, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.
Unit Tests повышают надежность кода и позволяют обнаруживать ошибки на ранних этапах разработки. При этом они не позволяет отловить все ошибки. К тому же Unit Tests, как и обычный код, нужно поддерживать.
Continuous Integration (CI, непрерывная сборка )
Непрерывная интеграция – это практика как можно более частой сборки всех модулей и изменений системы вместе. В обычном проекте, где над разными частями системы разработчики трудятся независимо, стадия интеграции является заключительной. И она может задержать окончание работ. Переход к непрерывной интеграции снижает трудоёмкость процесса и делает интеграцию более предсказуемой за счет наиболее раннего обнаружения и устранения ошибок и противоречий.
Преимущества:
- Быстрое выявление и исправление проблем интеграции.
- Немедленный прогон модульных тестов (Unit Tests).
- Постоянное развертывание целевого решения на тестовом стенде или на стенде разработчиков, что позволяет не тратить время участников проектной команды.
- Прогон интеграционных автотестов.
Из минусов стоит назвать относительно большие временные затраты на поддержку непрерывной интеграции и потенциальную необходимость в выделенном сервере под эти нужды.
У нас на проекте в НСПК сборка модулей происходила минимум один раз в день. Тесты в горячую пору запускались до 5–9 раз в день.
Нужно понимать, что вышеприведенные практики очень плохо работают поодиночке. Только в симбиозе они позволяют получить сильный прирост к эффективности и качеству кода на проекте. Последовательность всех указанных практик представлена на рис. 2.
Рис. 2. Последовательность практик тестирования кода
И напоследок приведем цифры и результаты, полученные нами на проекте. Мы поставили все сборки в срок, при этом сократили Time-To-Market в 15 раз: без автоматизированных тестов, включенных в цепочку CI, проведение полного цикла регресса заняло бы 30 календарных дней, а мы уложились в общей сложности в 2 календарных дня Проект содержит около 1 млн строк, 70% функционала которого было покрыто автотестами. До передачи в продуктив (за полгода) мы выявили более 1000 ошибок. В продуктиве же было обнаружено всего около 50 важных ошибок.