Привет, Хабр! Меня зовут Алексей, я занимаюсь разработкой приложений ПСБ для юридических лиц. В этой статье постараюсь опровергнуть мнение, что разработчики пренебрегают модульным тестированием из-за нехватки времени и нежелания выполнять монотонную работу. А также хочу показать, как данная практика снижает нагрузку на специалистов и сокращает трудозатраты.

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

Из чего состоит работа разработчика

Многие думают, что 80% рабочего времени разработчики пишут код. Но это заблуждение. Существует огромное количество сопутствующих задач, на которые в совокупности тратится больше времени, чем на код.

Давайте подробнее рассмотрим, из чего состоит работа разработчика:

  1. Анализ требований к задаче

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

  2. Поиск технического решения

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

  3. Разработка технического решения (то самое написание кода)
     Тут программист, основываясь на ТЗ и выбранном техническом решении, пишет код для внедрения нового функционала, либо поддержки или изменения старого.

  4. Подготовка артефактов

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

  5. CodeReview

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

  6. Подготовка к тестированию

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

  7. Поддержка функционала

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

  8. Подготовка к релизу 

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

  9. Регрессионное тестирование

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

  10. Поставка пользователям 

    Завершающий этап процесса – релиз, когда новый функционал раскатывают на пользователей.

  11. Сопровождение функционала

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

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

Как мы видим, непосредственно написание кода — это всего лишь одна из как минимум 11 инженерных задач разработчика.

Идею о том, что данный этап занимает незначительную часть общего времени разработки, подтверждают исследования. Результаты опроса, проведенного Tidelift и The New Stack, показали, что на написание нового и поддержку старого кода уходит примерно 33% рабочего времени. Остальное время распределено следующим образом:

Мотивация разработчиков

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

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

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

  2. Этап поиска технического решения в значительной степени связан с чтением чужого кода и документации к нему. Код не всегда написан понятно, а документация может отсутствовать, устареть, не отражать подробностей реализации или же быть написанной на языке бизнеса (не всегда до конца доступном разработчику).
     
    Модульные тесты, в свою очередь, можно интерпретировать как наглядную и простую документацию: она написана на языке разработчика, отображает все нюансы работы функционала, которые были зафиксированы на предыдущем этапе анализа требований к задаче. Разработчик сразу может на коротком примере разобраться, как работает тот или иной класс, и при этом быть на 100% уверенным в актуальности тестов, потому что иначе они бы выдавали ошибку.
     

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

    Ускорение разработки на данном этапе обусловлено следующими факторами: 

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

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

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

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

      Об этом пишет и Р. Мартин в своей книге «Чистый код». Рассуждая о формировании архитектуры, он выделяет несколько правил, среди которых есть самое главное: «Как ни удивительно, выполнение простого и очевидного правила, гласящего, что для системы необходимо написать тесты и постоянно выполнять их, влияет на соответствие системы важнейшим критериям объектно-ориентированного программирования: устранению жестких привязок и повышению связности. Написание тестов улучшает архитектуру системы».

  4. Этап Code review благодаря модульному тестированию в значительной степени упростится. Причем как со стороны разработчика, код которого проверяют, так и со стороны проверяющего.

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

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

Цена ошибки

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

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

Чем позже замечена ошибка, тем больше людей задействованы в ее исправлении. Например, на этапе Code Review исправляют ошибку три человека: автор кода и два ревьюера. Ошибка, замеченная на этапе тестирования, потребует уже четырех человек: добавится тестировщик. При выявление ошибки с помощью юнит-тестов используется только один человеческий ресурс – сам разработчик.

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

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

А теперь цифры

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

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

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

  3. Уровень покрытия кода тестами: чем больше кода покрыто тестами, тем больше ошибок они смогут выявить.

  4. Эффективность самих тестов: хорошо написанные тесты смогут выявить больше ошибок, чем плохо написанные.

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

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

Если все суммировать, то среднее количество обнаруженных ошибок в задачах, плохо покрытых тестами, составляет 4,5 против 0,5 в задачах, которые хорошо покрыты тестами.

На основании данных Jira, минимальное время на исправление одной ошибки равняется 4-6 часам. Добавьте к этому временные затраты на Code Review, доставку сборки до тестировщика – и получите целый рабочий день, затраченный на исправление лишь одного дефекта.

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

Выводы

В этой статье я показал преимущества модульного тестирования в разработке программного обеспечения на конкретном примере среднестатистического инженера-разработчика.

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

  • позволяет обнаружить и исправить ошибки на ранних стадиях разработки, что значительно сокращает затраты времени и ресурсов на их устранение;

  • способствует улучшению качества кода благодаря созданию позитивных ограничений для разработчиков;

  • упрощает процесс интеграции и документации кода, стимулирует разработчика к изменениям и рефакторингу.

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

Комментарии (5)


  1. aamonster
    26.07.2024 14:43

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


    1. Andrey_Solomatin
      26.07.2024 14:43
      +1

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


      1. aamonster
        26.07.2024 14:43
        +1

        Это как раз случай, когда тесты особенно полезны, хоть код в порядок приведёт :-)

        Но я говорил про другой случай, когда реализуется новый функционал. И описал фактически реальный случай из практики :-)


  1. Andrey_Solomatin
    26.07.2024 14:43

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


  1. Andrey_Solomatin
    26.07.2024 14:43

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