[Перевод] С 80-х по 2024-й: как создавались и оптимизировались CI-тесты

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

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

Самый медленный способ тестирования кода — чтение

В 1980-х годах тестирование программного обеспечения было медленным. Бо́льшая часть тестирования была сосредоточена прежде всего на поиске возможных ошибок в коде. Майкл Фейган популяризировал «инспекции Фейгана», во время которых группы разработчиков просматривали распечатки кода в поисках ошибок. Этот процесс отнимал много времени, выполнялся вручную и больше походил на интенсивное код-ревью, чем на то, что мы сегодня считаем тестированием программного обеспечения.

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

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

Периодическое самостоятельное тестирование

Опубликованная в 1999 году книга Кента Бека «Экстремальное программирование» (XP) помогла изменить культуру разработки ПО. Разработчиков поощряли писать небольшие изолированные тесты для каждой новой части кода, которую они вносили. 

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

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

Запуск автоматизированного сервера для тестирования изменений

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

Программные системы становятся больше и сложнее. Что ещё хуже, новые версии доставляются пользователям часто, иногда несколько раз в день. Это очень далеко от мира «коробочного ПО», которое обновляется лишь один или два раза в год.

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

Разработчики программного обеспечения в Google

Разработчик Sun Microsystems Косукэ Кавагучи сыграл ключевую роль в открытии новой эры тестирования. В 2004 году он создал Hudson, позже переименованный в Jenkins в результате конфликта с Oracle. На своей основной работе Косукэ «устал навлекать на себя гнев своей команды каждый раз, когда его код ломал сборку». Он мог бы вручную запускать тесты перед каждым внесением кода, но вместо этого Косукэ поступил как типичный программист: создал автоматизированную программу. Инструмент Hudson действовал как долгоживущий тестовый сервер, который мог автоматически проверять каждое изменение кода по мере его интеграции в кодовую базу.

3e4bb24578b6a4b4a809c475842572bf.png

Косукэ открыл исходный код Hudson, и тот стал пользоваться огромной популярностью. Началось первое поколение автоматизированных тестов непрерывной интеграции, и впервые стало обычным делом тестировать каждое изменение кода по мере его написания. Быстро развернулись подобные инструменты, например Bamboo и TeamCity, но опенсорсный Hudson был популярнее.

Платить кому-то другому, чтобы автоматически тестировать изменения

К концу 2000-х годов хостинг кода переместился в облако. Раньше команды запускали собственные серверы Subversion для размещения и интеграции изменений кода, теперь же всё больше людей стали размещать свой код на GitHub. Тесты непрерывной интеграции последовали этой тенденции и также переместились в облако: в 2011 году появились такие продукты, как CircleCI и Travis CI. Теперь и небольшие компании могли передавать на аутсорс обслуживание самих обработчиков заданий CI. Более крупные старые компании в основном оставались на Jenkins, потому что могли позволить себе продолжать поддерживать серверы CI самостоятельно и потому что Jenkins предлагал более продвинутый контроль.

В середине 2010-х годов мы стали свидетелями двух эволюций облачных систем CI.

  1. CI-системы, не требующие обслуживания, объединились со службами хостинга кода. GitLab был первым, кто предложил это универсальное решение: оно позволяло пользователям запускать свои тесты CI на той же платформе, на которой они ревьюили и мержили изменения. Microsoft приобрела GitHub в 2018 году и добилась релиза GitHub Actions, который поддерживался продуктом Microsoft Azure DevOps. В обоих случаях две наиболее популярные платформы для размещения кода изначально предлагали интегрированное выполнение тестов CI.

  2. Крупные организации перешли с Jenkins на более современные варианты самостоятельного размещения. Buildkite был первым популярным современным решением, запущенным в 2013 году. Он дал компаниям возможность получить преимущества веб-панелей управления и координации, при этом по-прежнему размещая свой код и выполняя тесты на собственных компьютерах. Позже GitHub и GitLab предложили свои самостоятельные обработчики заданий CI, а некоторые компании со множеством ручных тестов решили выполнять собственные тесты в конвейерах CodeDeploy в AWS или на платформе DevOps в Azure.

Процесс тестирования программного обеспечения можно рассматривать с точки зрения скорости и дешевизны:

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

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

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

  • Минуты. Примерно в 2011 году стали доступны сервисы тестирования CI, не требующие обслуживания. Теперь тестирование каждого изменения стало выгодно и небольшим командам.

Сокращение времени на тестирование одного изменения

Лучшие практики направлены на то, чтобы время CI составляло около 10–15 минут: таким образом разработчики могут поддерживать короткие итерации. Но это становится всё сложнее, потому что кодовые базы и наборы тестов растут с каждым годом.

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

Разработчики программного обеспечения в Google

Есть только три способа ускорить что-либо в программном обеспечении: вертикальное масштабирование, распараллеливание и кеширование. В случае с CI применяют все три способа, причём в последние годы всё больше внимания уделяется кешированию и распараллеливанию.

Во-первых, десятилетиями закон Мура гарантировал, что всё более мощные процессоры смогут быстрее выполнять наборы тестов, хоть и с большими затратами. Используя облачные сервисы по требованию, разработчики могут переключить настройку в AWS или GitHub Actions, чтобы оплатить более мощный сервер, и надеяться, что их набор тестов будет выполняться быстрее.

Во-вторых, поставщики CI постепенно совершенствовались в распараллеливании. Buildkite, GitHub Actions и другие поставщики позволяют пользователям определять графы зависимостей этапов тестирования, что даёт возможность разным компьютерам передавать контекст и выполнять тесты параллельно для одного и того же изменения кода. Облачные вычисления позволяют организациям выделять бесконечное количество параллельных хостов для выполнения тестов, не опасаясь нехватки ресурсов. Наконец, сложные инструменты сборки, например Bazel и Buck, позволяют большим базам кода вычислять графы сборки, а также распараллеливать сборку и тестирование на основе графа зависимостей в самом коде.

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

Увеличение скорости тестирования всех изменений

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

И всё же шаблоны разработки продолжают оптимизироваться для повышения скорости.

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

Ответ: совсем не запускать для этого изменения некоторые тесты.

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

Chromium использует вариант этого подхода под названием Commit Queue, чтобы главная ветка была всегда зелёной. Изменения, прошедшие первый этап, отбираются для второго этапа каждые несколько часов, чтобы пройти большой набор тестов, который занимает около четырёх часов. Если сборка сломается на этом этапе, весь пакет изменений будет отклонён. Шерифы сборки и их помощники подключаются в этот момент, чтобы приписать сбои конкретным ошибочным изменениям. Обратите внимание, что такой подход приводит к релизу пакетов, а не отдельных коммитов.

Второе место, где можно пропустить CI, — это многослойные изменения кода, которые так популяризировал Facebook*. Если разработчик выстраивает серию небольших пулл-реквестов, он неявно описывает требуемый порядок мержа этих изменений. Как и в очереди мержей, CI можно объединить с этими изменениями и искать проблемное среди них бинарным поиском (git bisect), если будут обнаружены какие-либо сбои. Сбои в нижней части стека могут уведомить разработчиков ещё до начала выполнения изменений вверх по стеку.

Если раньше графы зависимостей при тестировании предлагали раннее обнаружение отказов и экономили вычислительное время при тестировании одного изменения, то теперь они дают те же преимущества и для нескольких пулл-реквестов. Экономия вычислительного времени имеет большое значение: несмотря на то, что облачные ресурсы предлагают бесконечную горизонтальную масштабируемость, расходы на тестирование могут составлять до 10–20% от общих расходов компаний на облачные вычисления.

Тестирование быстрее, чем выполнение кода

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

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

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

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

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

* Facebook — проект Meta Platforms, Inc., деятельность которой в России запрещена.

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

Или открыть перспективы с профессиональным обучением и переподготовкой:

© Habrahabr.ru