Системный подход Возможно, самый важный аспект построения систем, который я начал ценить с тех пор, как переключил свое внимание с академических занятий на разработку программного обеспечения с открытым исходным кодом, — это важность тестирования и автоматизации тестирования.
В академических кругах не будет большим преувеличением сказать, что мы учим студентов тестированию только в той мере, в какой нам нужны тестовые примеры для оценки их решений, и мы заставляем наших аспирантов проводить тесты производительности для сбора количественных данных для наших исследовательских работ, но это довольно много это.
Конечно, есть исключения – например, учебные программы, ориентированные на программную инженерию, – но мой опыт показывает, что значение, придаваемое тестированию в академических кругах, не соответствует его важности на практике.
Я сказал, что ценю роль тестирования программного обеспечения, но не уверен, что понимаю это достаточно ясно и глубоко, чтобы объяснить это кому-либо еще. В силу характера нашего системного подхода мне хотелось бы лучше понять «почему», но в основном то, что я вижу и слышу, — это много жаргона: юнит-тесты, дымовые тесты, тесты на выдержку, регрессионные тесты и т. д. интеграционные тесты и так далее.
Проблема с этой и подобной терминологией заключается в том, что она носит скорее описательный, чем предписывающий характер. Конечно, интеграционный тест (например) — это хорошо, и я понимаю, почему вы можете обоснованно утверждать, что конкретный тест является интеграционным тестом, но я не уверен, что понимаю, почему он необходим или достаточен в общей схеме вещи. (Если этот пример слишком неясен, вот еще один пример, опубликованный пользователем Reddit.)
Исключением могут быть модульные тесты, где покрытие кода является количественным показателем, но даже в этом случае, по моему опыту, больше внимания уделяется способности измерять прогресс, чем ее реальному вкладу в создание качественного кода.
На этом фоне я недавно обнаружил, что пытаюсь выполнить сортировку более чем 700 заданий по обеспечению качества (с большими ежемесячными расходами на AWS), накопившихся за последние пять лет в проекте Aether.
Я не думаю, что конкретная функциональность особенно важна — Aether состоит из четырех подсистем на основе микросервисов, каждая из которых развернута как рабочая нагрузка Kubernetes в пограничном облаке — хотя, вероятно, важно, что подсистемы управляются как независимые проекты с открытым исходным кодом, каждая из которых имеет своя команда разработчиков. Однако проекты используют общие инструменты (например, Jenkins) и подключаются к одному и тому же конвейеру CI/CD, что делает его достаточно репрезентативным для практики построения систем путем интеграции нескольких исходных источников.
Из моего «тематического исследования» становится ясно, что речь идет о нетривиальных компромиссах, когда конкурирующие требования тянутся в разных направлениях. Одним из них является противоречие между скоростью выполнения функций и качеством кода, и именно здесь ключевую роль играет автоматизация тестирования: предоставление инструментов, которые помогут инженерным группам реализовать и то, и другое.
Лучшая практика (которую применяет Aether) — это так называемая стратегия «Сдвиг влево»: введение тестов как можно раньше в цикле разработки (т. е. ближе к «левому» концу конвейера CI/CD). Но «Сдвинуть влево» проще в теории, чем на практике, потому что тестирование требует затрат как по времени (разработчики ждут запуска тестов), так и по ресурсам (виртуальные и физические машины, необходимые для запуска тестов).
Что происходит на практике?
На практике я видел сильную зависимость от разработчиков, вручную выполняющих функциональные тесты на уровне компонентов. Это тесты, о которых думает большинство людей, когда думают о тестировании (и когда публикуют шутки о тестировании на Reddit), а независимые инженеры по обеспечению качества приносят пользу, выявляя проблемы, которые упускают из виду разработчики, но при этом не могут предвидеть критические крайние случаи.
В случае с Aether один из ключевых функциональных тестов проверяет, насколько хорошо разработчики реализовали спецификацию протокола 3GPP. Эта задача настолько сложна, что тесты обычно приобретаются у стороннего поставщика. Что касается автоматического тестирования, конвейер CI/CD выполняет в основном формальные тесты (например, проверяет ли он сборку, имеет ли он соответствующее уведомление об авторских правах, подписал ли разработчик CLA) в качестве ворот к объединению патча с базой кода.
Это налагает тяжелое бремя на интеграционные тесты после слияния, где ключевой проблемой является обеспечение достаточного «охвата конфигурации», то есть проверка того, что независимо разработанные подсистемы настроены таким образом, чтобы представить, как они будут развернуты как единое целое. . Покрытие единицы является простым; охвата всей системы нет.
Для меня ключевым моментом является осознание того, что управление конфигурацией и эффективность тестирования глубоко взаимосвязаны. (Именно поэтому автоматизация конвейера CI/CD так важна.)
Чтобы сделать это немного более ощутимым, позвольте мне использовать конкретный пример из Aether (который, я не считаю, уникальным).
Чтобы протестировать новую функцию — например, возможность запуска нескольких функций пользовательской плоскости (UPF), каждая из которых обслуживает отдельный раздел (срез) беспроводных устройств — необходимо развернуть комбинацию (а) мобильного ядра, реализующего UPF. , (b) контроллер времени выполнения, который привязывает устройства к экземплярам UPF, и (c) генератор рабочей нагрузки, который отправляет значимый трафик через каждый UPF.
Каждый из трех компонентов поставляется со своим собственным «файлом конфигурации», который интеграционный тест должен согласовать таким образом, чтобы получить сквозной результат. В слабосвязанной облачной системе, такой как Aether, интеграция равна скоординированной конфигурации.
Теперь представьте, что вы делаете это для каждой новой функции, которая появляется, и либо количество уникальных конфигураций резко возрастает, либо вы выясняете, как добиться сходимости адекватных функций, выборочно решая, какие комбинации тестировать, а какие нет.
У меня нет хорошего ответа на вопрос, как это сделать, но я знаю, что это требует как внутренних знаний, так и суждений. Опыт также показывает, что многие ошибки будут обнаружены только в процессе использования, что говорит мне о том, что предварительное тестирование и наблюдение после выпуска тесно связаны. Рассматривать управление выпусками (включая поэтапное развертывание) как еще один этап стратегии тестирования — это разумный целостный подход.
Возвращаясь к тому, с чего я начал — пытаясь понять тестирование программного обеспечения через призму системы — я не думаю, что удовлетворил свои личные критерии приемлемости. Есть несколько принципов дизайна, с которыми нужно работать, но задача по-прежнему кажется равнозначной частью искусства и инженерии.
Что касается сортировки, которую я намеревался выполнить на наборе заданий по контролю качества Aether, я все еще пытаюсь отделить зерна от плевел. Это естественное следствие развития системы без четкого плана по отключению устаревших тестов. Эта работа еще находится в стадии разработки, но один очевидный вывод заключается в том, что как студентам, так и практикам будет полезно иметь строгую основу в тестировании программного обеспечения. ®