Автоматизированное тестирование для молодых и дерзких

— Мы напишем тесты потом...
— Простите, я не куплю это.

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

  • Сейчас мы пишем код, а тесты напишем потом, потому что сейчас все меняется, а вот когда перестанет меняться, вот тогда и напишем.
  • Давайте не будем писать тесты, так мы сможем двигаться быстрее, тесты нас замедлят.
  • К сожалению, на написание тестов времени нет, будем писать только код… Если время останется, напишем тесты.

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

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

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

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

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

Сначала проект развивался какими-то супер-разработчиками (уровня “бог”), которые очень-очень умные и профессиональные. У них был божественный план, что вело к быстрому набору проектом новых функций (Gods Era).

В определенный момент в команду были включены хорошие, крепкие разработчики, которые принялись изучать систему и активно отвлекать гуру от насущных забот. Началась эра принятия (Adoption Era), в течение которой новые фичи стали появляться реже, поскольку разработчики больше занимались тем, что пытались понять как это божественное творение развивать (часто без грамма документации и скупую помощь богов, которых вообще на другой проект перевели).

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

Однако, со временем из команды начали уходить сотрудники - кому-то перестало нравиться писать на Java 5 (а на дворе уже Java 9), кого-то перевели в другой проект. В результате, у команды снизился уровень компетенций. Да и сам продукт стал таким сложным, что внедрение новых функций стало затрагивать множество изменений. Наступила эра декаданса (Decadence).

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

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

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

Качество программного продукта

Чтобы понять, почему автоматизированное тестирование - самый важный компонент в ряду практик продления ренессанса, стоит начать с понимания качества. Качество, по классике - соответствие ожиданиям. Вопрос стоит в том, чьим ожиданиям необходимо соответствовать? Ответ простой - ожиданиям всех заинтересованных лиц: разработчика, инженеров QA, тимлидера, менеджера проекта, менеджера продукта, владельца продукта, пользователя. Для всех важно качество, но каждый понимает его по-своему. Рассмотрим пример, который может быть актуальным для участников некоторого проекта.

Пользователь продукта

  • Нет глюков,
  • работает быстро,
  • удобный,
  • выгодный.

Владелец продукта

  • Соответствует требованиям,
  • нет ошибок,
  • пользователям нравится.

Менеджер продукта

  • Соответствует требованиям,
  • нет ошибок,
  • соответствует ожиданиям владельца продукта,
  • соответствует ожиданиям пользователей.

Менеджер проекта

  • Соответствует требованиям,
  • высокое покрытие тестами (coverage),
  • все сборки завершаются успешно,
  • регрессионное тестирование не выявляет повторных дефектов,
  • тесты не выявляют дефектов в незатронутых компонентах,
  • покрытие всех пользовательских сценариев smoke тестами.

Тим лидер

  • Unit-тесты реализованы качественно и обеспечивают высокое покрытие (coverage),
  • интеграционные тесты реализованы качественно и всеобъемлюще,
  • smoke-тесты интегрированы в CI-пайплайн и выполняются вместе с остальными тестами,
  • при запросах на слияние тесты проходят успешно,
  • ветки develop и master успешно собираются, публикуются и развертываются,
  • код реализован с использованием лучших практик разработки программного обеспечения,
  • все слитые изменения успешно прошли ревью.

Инженер QA

  • Тестовый план полон и актуален,
  • регрессионное тестирование успешно проходит согласно тестовому плану,
  • Автоматизированные smoke тесты успешно выполняются при интеграции,
  • Покрытие тестового плана автоматизированными тестами высокое.

Разработчик

  • Код соотвествует стандартам, принятым в команде,
  • код реализован согласно заданию,
  • классы и методы покрыты юнит-тестами,
  • для комплексных взаимодействий реализованы интеграционные тесты,
  • при отправке кода, в PR нет замечаний от принимающего,
  • при выполнении CI/CD проходят все стадии проверки в системе интеграции.

Восприятие качества участниками может быть другим и включать другие критерии, которые в данной таблице не представлены. В реальных условиях все участники в разной степени заинтересованы в качестве: некоторым требуются ощутимые способы измерить качество, другими участниками качество воспринимается как результат успешности продукта. Однако, есть и такие субъекты, которые это качество как раз и создают - разработчики. По иронии судьбы, стечению обстоятельств, нехватке времени, лени, самоуверенности или другим причинам, именно разработчики считают, что их продукт является качественным просто потому, что это они его делают, даже, если они не применяют никаких практик, методов и инструментов оценки и улучшения качества. Еще раз коротко: “продукт качественный, потому что я его делаю”.

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

Связь тестирования и производительности команды

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

Таким образом, существует как минимум две корневых причины ошибок:

  • неправильное решение, которое привело к ошибке;
  • правильное решение, которое привело к ошибке, поскольку не все взаимодействующие элементы были подвержены соответствующему изменению.

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

Таким образом, ухудшение качества - это прямое следствие внесения изменений в систему. Нельзя внести изменение в систему и не ухудшить качество. Любые изменения вызывают временную потерю качества. Вопросы заключаются в следующем:

  • где качество упало;
  • как это качество вернуть на прежний уровень;
  • сколько времени это займет.

Знание ответа на эти вопросы в каждый момент времени определяет продолжительность эры ренессанса при разработке продукта.

Давайте представим простой вариант для примера: разработчик пишет код, инженер QA проверяет функцию, раз в две недели происходит регрессионное тестирование. Рассмотрим задержку по внесению данного изменения в продукт.

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

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

В примере вы увидели, что только тестирование компонента, вкупе с регрессионным тестированием, которое позволило проверить и связанные компоненты, позволило найти ошибки и исправить их. Таким образом, только тестирование способно ответить на вопрос “Где упало качество?”. Обратите внимание, оно отвечает не только на вопрос соответствия качества ожидаемому перед доставкой пользователю, но и важно как инструмент, который позволяет понять где качество упало.

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

Само собой, что в идеальном случае, всем бы хотелось, чтобы области снижения качества выявлялись по мере их возникновения, так быстро как возможно, чтобы можно было обнаружить не только место непосредственной ошибки, но и влияние ее на связанные компоненты. Представьте, что разработчик просто не может интегрировать свои изменения в продукт, потому что в них низкое качество, что подтверждено явно, без субъективизма и допущения. Добиться такого результата позволяет только обширное автоматизированное тестирование.

Комплексное автоматизированное тестирование

Комплексное автоматизированное тестирование покрывает следующие уровни программного обеспечения:

  • отдельные классы, методы, функции - unit-тесты;
  • модули, взаимодействующие объекты - интеграционные тесты;
  • пользовательские сценарии - smoke-тесты (e2e-тесты).

Unit-тесты и интеграционные тесты должны создаваться разработчиками, поскольку они отражают внутреннюю архитектуру приложения. Smoke-тесты (e2e) создаются инженерами QA и отражают восприятие продукта реальными потребителями (соответствие внешней спецификации).

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

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

Общая практика такова, что разработка с использованием обширного автоматизированного тестирования в обязательном порядке включает в себя следующие задачи:

  • для класса создаются и поддерживаются unit-тесты;
  • для взаимодействия класса с другими классами создаются и поддерживаются интеграционные тесты;
  • при нахождении ошибки создаются специфические unit-тесты и интеграционные тесты для данной ошибки;
  • для каждого сценария пользовательского взаимодействия с системой создаются e2e тесты;
  • для каждой найденной ошибки создается e2e тест.

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

Работа с заблуждениями об автоматизированном тестировании

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

“У нас нет денег на это”

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

“Мы сдаем проект, надо покрыть код тестами”

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

“Тесты нас замедляют, без тестов мы будем двигаться быстрее”

Еще одно заблуждение. Если Вы внимательно читали статью, Вы уже понимаете, что все будет с точностью до обратного, через 1-2 месяца, проект превратится в мучение, в колосс на глиняных ногах, времени на анализ областей ухудшения качества будет уходить все больше и больше.

“У нас сейчас все так быстро меняется, когда мы стабилизируем архитектуру, тогда и напишем тесты”

В переводе на правильный язык: мы сейчас будем делать так, как нам нравится, а потом напишем тесты для того, что сделали, чтобы Вы были счастливы. Ни за что не покупайте это.

На самом деле нельзя разрабатывать тесты “потом”

Наверное, есть и другие аргументы, которые могут приводиться противниками внедрения автоматизированного тестирования. Я бы сказал, что внимания заслуживает только один из них: Мы не умеем это делать. Это тот аргумент, который Вы должны уважать и помочь команде преодолеть эту проблему, помочь научиться. Аппетит, как известно приходит во время еды. Многие люди просто не могут начать писать тесты, пока им не помогут жить в этой среде, например, проведя тренинг по TDD (test-driven development). Этому есть причина. Если разработчик никогда не разрабатывал программное обеспечение с применением тестирования, то его код просто не подходит для тестирования.

Другими словами, существует код, который невозможно протестировать качественно. Только разработка, с оглядкой на тестирование, позволяет разработать код, который возможно тестировать. Подход разработки, например, TDD, определяет дизайн кода, когда разработчику необходимо создавать unit-тесты, интеграционные тесты, он вынужден думать о том, как создать такой код, который легко протестировать, в итоге, в коде начинают сами собой появляться лучшие практики: компактные функции, неизменяемые объекты (immutability), инверсия зависимостей, отсутствие глобальных объектов. Это просто следствие того, что код необходимо тестировать. Это, на самом деле, уникальное явление, которое настолько простое как философская максима “Бытие определяет сознание”: “Тесты определяют дизайн кода”. Таким образом, тесты из инструмента обнаружения ухудшения качества превращаются в инструмент повышения качества за счет лучшего дизайна.

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

Читайте еще по этой теме:

Если вам понравился этот пост, поделитесь им с друзьями.