[Перевод] Защищено ли ваше программное обеспечение?

В нашем мире, где каждый релиз готовится в спешке, а сами релизы выходят на регулярной основе, что вы делаете, чтобы защитить своё программное обеспечение? Что вы можете сделать, какие вообще есть варианты?

С точки зрения того, что именно представляет собой CI/CD:

Непрерывная интеграция (CI, Continuous Integration) программного обеспечения — это процесс, целью которого является (…) максимально проверенный (…) дистрибутив.

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

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

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

Цель тестов состоит в следующем: когда хотя бы один из них не срабатывает, остановиться и устранить дефекты и баги, чтобы не допустить нарушения SLA и SLO.

Автоматизированные тесты это тесты, которые выполняются по написанному коду.

Поэтому весь код делится на две разные части:

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

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

Кто стоит на страже?

Кто сторожит сторожа?

Кто сторожит сторожа?

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

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

Можно спорить о том, так ли важна эффективность тестового кода, как эффективность продакшен-кода. Однако ошибка в тестовом коде с большой вероятностью приведет к ошибке в продакшене…

Я сам выступаю против двойного стандарта:

Любой код — это код, и он должен соответствовать всем применимым к нему стандартам.

SUT = System Under Test

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

В литературе вы найдете множество форм (X)UT, где X может быть: C-Class, Code, D-Deployment, E-Environment, F-Function, H-Hardware, M-Module, P-Process, Product, Platform, U-Unit, S-Service, Server, Software и так далее, — все они являются частными случаями системы. Так что да. SUT.

Тестовый код

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

Правильно выполненные автоматизированные тесты могут выразить требования и стать автоматизированным критерием квалификации продакшен кода. Это привело к тенденции писать их первыми (TDD, BDD). Некоторые инструменты даже стремятся сделать их человекочитаемым текстом — доступным для специалистов, не обладающих навыков написания кода (например, Cucumber & Gherkin).

Храповик

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

Повышайте планку после каждой победы

Повышайте планку после каждой победы

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

Теперь, когда мы определились с терминами, давайте рассмотрим основные типы тестов, применимые к интеграционным потокам.

Статический анализ кода

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

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

Полные наборы правил образуют политику.

Пользовательские политики обычно являются частью вспомогательного кода в кодовой базе.

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

Линтеры

7041202f34f03b9f6258531483a0aef0.png

Инструмент линтер направлен в основном на форму кода. Политики линтеров называются Code-Styles.

Типы правил
Большинство правил подчиняется одному из этих мотивов:

  • Запретить формы, которые, как известно, подвержены багам. Например, неиспользуемые переменные, используемые необъявленные переменные, необработанные ошибки, заблокированные ошибки, избыточно вложенные выражения inline-if, сложные выражения, злоупотребление абстракциями и многое другое.

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

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

Вечный компромисс

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

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

Инструменты линтинга для каждого файла

Каждый язык программирования поставляется со своими собственными стилями и инструментами линтинга. Но всё же часто можно увидеть, как линтеры одного языка используются применительно к другим языкам, особенно вспомогательным или языкам разметки без среды выполнения, таким как HTML, CSS, JSON, YAML, Markdown и другие.

Линтинг старого репозитория

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

Сканирование зависимостей

8f6aae309496bafc48f5e0e41e8a5f1d.png

Запретите использование устаревших зависимостей или зависимостей с известными проблемами (обычно это эксплойты безопасности).

Чем старше версия — тем больше времени у хакеров было на поиск эксплойтов, и тем меньше в ней исправлений для эксплойтов, которые были найдены ранее.

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

Сканирование лицензии

Программное обеспечение с открытым исходным кодом (OSS) поставляется с лицензией, которая определяет его применимое законное использование, однако не все лицензии одинаковы: некоторые из них могут наделить владельца OSS правом на претензии, которые могут разрушить бизнес-модель. 

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

Сканирование безопасности

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

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

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

Сборка / компиляция

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

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

Но поверх этого сканирования у него есть целевая задача:

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

Таким образом, поверх отчёта об отказах и предупреждениях — в случае успеха — производится сборка.

Функциональные тесты

Показывают, что SUT делает то, что от него ожидается — то есть, что он функционален.

Слон в комнате

Слон в комнате

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

Грубо говоря, они делятся на:

  • юнит-тесты, которые проверяют каждый юнит в отдельности и хороши в деталях;  

  • сквозные (E2E) тесты, которые проверяют, что всё это работает вместе;  

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

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

Минимальное покрытие

Это инструменты, которые следят за SUT, пока он тестируется тестовым кодом. Они могут рассказать обо всех частях SUT, которые играли роль в этом тесте, и о том, сколько раз были задействованы.

Они дают детализацию по покрытию и итоговый результат — общий процент протестированного — он же покрытие. В идеале это делается после консолидации данных о покрытии из ВСЕХ тестов в кодовой базе.

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

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

Коэффициент тестового покрытия — наиболее распространенный «храповик», гарантирующий, что покрытие может только расти (например, coveralls, codecov, sonarqube).

Тесты производительности

Предположим, ваш iRobot/Rumba даёт чистый конечный результат. Но успеет ли он сделать это вовремя? Тесты производительности задают аналогичные вопросы относительно программного обеспечения.

Бенчмарки

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

14480ee2bcbdfeb5f16eaf7a77f1dc3e.png

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

Однако вне CI — можно сравнить SUT с конкурентами…

Нагрузочные тесты

Доказательство того, что SUT, который должен выполнять параллельные задачи, способен это делать.

Поддерживать всё в рабочем состоянии

Поддерживать всё в рабочем состоянии

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

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

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

Стресс-тестирование

23ca3d02663591abb22b096020c23e2b.png

Стресс-тест подвергает SUT значительному воздействию.

Системы, созданные для работы в стрессовых условиях, смогут во время стрессовой ситуации пожертвовать некоторыми своими функциями, чтобы избежать полного прекращения работы. То есть будет лучше, если система, находящаяся в стрессовом состоянии, ответит на запрос в форме «Извините, не могу помочь вам сейчас» или «Я записал ваш запрос. Пожалуйста, вернитесь позже», чем умрет при обработке запроса. И стресс-тесты призваны проверить, что система знает, что нужно поступить именно так.

Тест на утечку данных

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

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

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

Тесты на совместимость

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

Микросервисы точно знают, на какой платформе они работают и какие версии узлов они ожидают — обычно известно, куда они поставляются. Этого нельзя сказать о программном обеспечении, которое поставляется пользователям: библиотеках, мобильных приложениях и в некоторой степени — веб-приложениях.

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

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

Тестовые сетки удобно заказывать у сторонних сервисов (например, browserstack, saucelabs).

Заключение

Последняя цель — проверка дистрибутивов, предотвращение рисков для SLO, угроз безопасности и ошибок в продакшене.

Этот список далеко не полный. Количество тестов, которые можно провести, не ограничено, однако бюджет ограничен по определению.

После интеграции для поддержки и запуска тестов в каждой сборке требуются ресурсы. Даже если у вас есть возможности и деньги, все равно придётся выполнять столько тестов в сборке, сколько позволяет согласованное время сборки. Тесты, которые не получается выполнить в сборке, выполняются либо по расписанию, либо в процессе поставки.

Выбор тестов и времени их запуска — это основная проблема проектирования CI-процессов, которые помогают создать надёжное программное обеспечение.

Освоить все виды тестирования на профессиональном уровне, изучить актуальные инструменты и лучшие практики можно на онлайн-курсах в OTUS.

© Habrahabr.ru