Путешествие к более надежному программному обеспечению, выпущенному ранее

Общая иерархия тестов

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

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

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

Этот список соответствует архитектуре портов и адаптеров, популяризированной Робертом Мартином в его книге.

Модульные тесты

Нет единого определения модульного теста. Для меня модульный тест - это тот, который полагается только на четкие и конкретные абстракции. Диапазон, в котором они работают, обычно невелик. Лакмусовая бумажка состоит в следующем: когда ваши тесты достаточно «единичны», вам не нужно отлаживать. Если вам нужно, проблема, вероятно, не в ваших тестах, а в вашем дизайне. Создавайте более мелкие классы, покрывайте их тестами. Если вы можете обнаружить ошибку за считанные секунды, примите наши поздравления: вы создали достойный набор модульных тестов. Это очень важно, поскольку при проведении тестов каждые пять секунд или около того не должно быть напряжения.

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

Интеграционные тесты

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

На практике чаще всего они подразумевают тестирование либо контроллеров (или Application Services, на жаргоне DDD), либо конечных точек. Как правило, это означает, что вы имитируете вызовы сторонних сервисов, но не имитируете базу данных, используя вместо этого реальную. Будучи тестом более высокого уровня, вы не тестируете детали его реализации, то есть поведение его внутренних классов. Вместо этого вы проверяете правильность ответа HTTP и наличие желаемых побочных эффектов.

Будьте очень осторожны, чтобы оставаться на том же уровне абстракции. Например, если вы тестируете конечную точку регистрации заказа, не проверяйте, есть ли запись в базе данных. Это деталь реализации. Сегодня вы используете Postgres, а завтра вы заканчиваете Mongo. Вместо этого вызовите конечную точку API сведений о заказе, чтобы убедиться, что она содержит все необходимые данные. Какой в ​​этом весь смысл? Интеграционные тесты - ваша подстраховка. Когда детали реализации меняются, иногда резко, вы хотите убедиться, что все работает должным образом.

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

E2e тесты

Сквозные тесты - это последняя линия защиты. Они тестируют полный цикл взаимодействия с клиентом, от пользовательского интерфейса до серверной части, снова до пользовательского интерфейса и т. Д. При их написании я всегда помню принцип Парето: 20% любых усилий дают 80% результата. Определите свои ключевые бизнес-процессы и автоматизируйте их. Обычно именно он приносит вашему бизнесу большую часть денег. Например, оформление заказа и его дальнейшая обработка, в результате чего его доставка. Таким образом, с такими тестами в вашем наборе тестов у вас нет шансов внести серьезную ошибку, поэтому ваша спина будет прикрыта.

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

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

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

Люди часто объединяют тесты e2e с тестами пользовательского интерфейса, что является более широким понятием.

UI тесты

UI-тесты, вероятно, заслуживают отдельной пирамиды. В конце концов, принципы те же. При разработке пользовательского интерфейса большинство тестов обычно небольшие, быстрые, а те, которые работают в небольшом объеме, обычно попадают в категорию «юнитов». Другими словами, этот термин часто используется в качестве внешнего эквивалента модульного теста, который обычно применяется при разработке серверной части.

Как правило, в пользовательском интерфейсе нужно протестировать две вещи. Первый - это поведение. Существует множество примеров функциональности, которые можно охватить с помощью модульных тестов пользовательского интерфейса. При вычислении общей суммы заказа ссылка на правильный URL-адрес, предупреждение отображается красным цветом и т. Д. Кроме того, вам не нужно тестировать большую часть поведения сквозным способом, даже если вам нужен ответ от бэкэнда. Напишите заглушку, и все готово. Его вполне можно протестировать изолированно.
Jasmine и Mocha - хорошее место для начала с модульных тестов пользовательского интерфейса.

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

Испытания в рамках непрерывной доставки

Если вы хотите часто и надежно развертывать свое программное обеспечение, одного тестового портфеля недостаточно. Ключ в том, чтобы автоматизировать это. Вы должны иметь возможность развертывать свой код в любое время дня и ночи. Вы хотите положиться на людей, которые могут забыть запустить тесты перед запуском кода? Вы хотите развернуть свой код только для того, чтобы обнаружить, что он не компилируется? Готов поспорить, что нет. Итак, лучший совет, который вы можете получить, - использовать конвейер CI / CD, потому что он исключает человеческий фактор.

Кроме того, подготовка и развертывание инфраструктуры - довольно утомительные и повторяющиеся задачи, с которыми мы, люди, особенно плохо справляемся. Напротив, конвейеры CI / CD отлично справляются с автоматизацией этих задач. Ручные задачи не масштабируются. Если вам сейчас нужен один ручной тестер, по мере роста вашей системы их будет все больше. Таким образом, вместо того, чтобы тратить все больше времени на их выполнение, вы можете автоматизировать все свои процессы раз и навсегда.

Общие ловушки

Тесты, запускаемые вручную
Отсутствие автоматизации тестирования - отличный рецепт того, как отказаться от тестирования вашей команды. Пару раз, когда кто-то из членов команды забывает запустить тесты перед запуском кода, может быть достаточно, чтобы разочароваться во всем тестировании. Как не делать этого? Используйте правильный конвейер CI / CD, который никогда не забывает запускать ваши тесты перед развертыванием.

Нестандартные тесты
Нестандартные тесты - это тесты, которые время от времени не проходят. Есть несколько проблем с такого рода тестами.

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

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

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

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

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

Желание протестировать частный метод
Это явное указание на то, что класс, которому принадлежит этот частный метод, нарушает принцип единой ответственности. Это объект Бога, который слишком много знает и слишком много делает. Лекарство простое: выделите этот частный метод в его собственный класс и накройте его модульными тестами.

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

Структура тестов в точности повторяет структуру классов
На первый взгляд это кажется логичным, но посмотрите на это с другой стороны. Некоторые из ваших тестов должны служить вам защитной сеткой, которая гарантирует, что поведение вашей системы не изменится, когда вы измените ее внутреннюю реализацию. Чаще всего большинство наших низкоуровневых классов представляют эту внутреннюю реализацию. Таким образом, любое изменение в этом классе приведет к соответствующему провалу теста. Исправлять это каждый раз - непростая задача. Роберт Мартин рекомендует тестировать противоречивость, что означает тестирование только наблюдаемого поведения класса. Это означает, что если ваш класс A, обеспечивающий поведение, зависящее от предметной области, использует набор стабильных помощников, зависящих от реализации, вам не нужно их тестировать. XTest было бы хорошо. Так что нет необходимости тестировать всю внутреннюю реализацию, это очень хрупко.

В заключение

Это действительно огромная тема, и я едва коснулся ее поверхности. Я предлагаю вам прочитать об этом еще; Развитие объектно-ориентированного программного обеспечения под руководством тестирования и Искусство тестирования программного обеспечения - отличные источники дальнейшего вдохновения.