В. В. Кулямин
Труды Института системного программирования РАН
Аннотация. Статья посвящена различным способам организации и структуризации сложных тестовых наборов. Анализируются основные проблемы эксплуатации и развития тестов, для решения которых необходимо введение дополнительной структуры. Рассматриваются три базовых техники структуризации тестов — выделение модулей, определение квалификаторов и введение конфигурационных параметров.
Современное программное обеспечение (ПО) очень сложно, и сложность его постоянно возрастает. Это вызвано естественным стремлением человечества к развитию, к решению новых, все более сложных задач и, соответственно, постоянно возрастающими требованиями к функциональности и удобству используемых на практике программных систем. Поэтому рост сложности практически значимого ПО стоит рассматривать не как проблему, нечто нежелательное, а как объективный фактор, один из вызовов, стоящих перед способностями человека к познанию законов окружающей действительности и умению использовать их.
Сложные программные системы требуют соответствующих их сложности и масштабируемых методов обеспечения надежности и качества. Единственным практически работающим подходом к созданию надежных программных систем является метод корректирующих уточнений, при котором система строится постепенно из небольших частей, при этом каждый компонент системы, каждая их группа и система в целом проходят многочисленные проверки, позволяющие выявлять расхождения с требованиями и шаг за шагом устранять их. Для проверки выполнения требований и обнаружения ошибок чаще всего используется тестирование.
Однако, с ростом сложности систем, сложность разработки и поддержки в рабочем состоянии тестовых наборов для них растет гораздо быстрее. Современные методы разработки ПО позволяют с разумными трудозатратами создавать системы объемом до десятков миллионов строк кода [1,2], хотя еще двадцать лет назад эта планка была на уровне десятков тысяч строк. В то же время используемые на практике техники создания тестов за это время увеличили свою масштабируемость лишь примерно на порядок, хотя тестовый набор для сложной программной системы сам по себе также является сложной системой. Возникающее расхождение между масштабами систем, которые мы можем создать, и систем, которые мы в состоянии аккуратно проверить, грозит увеличением количества сбоев в ПО и ущерба от них. О масштабах возникающих проблем говорят хотя бы следующие цифры, относящиеся к различным программным продуктам компании Microsoft. Тестовый набор для текстового редактора Microsoft Word XP содержал около 35 тысяч тестов [3], а для Windows XP — более 2-х миллионов. Если в разработке Windows NT 4.0 участвовало около 800 служащих компании, а в ее тестировании — около 700, то у Windows 2000 было около 1400 разработчиков и 1700 тестировщиков, т.е. соотношение программистов и тестировщиков поменялось на обратное [4].
Еще более важным фактором увеличения сложности тестовых наборов является необходимость их развития, следующего за развитием тестируемой системы. Любое практически значимое ПО проходит достаточно долгую эволюцию, постепенно расширяя и изменяя свои функции и пользовательский интерфейс. Соответственно, для снижения затрат на сопровождение и обеспечение большей предсказуемости этого развития аналогичный путь должен проходить и тестовый набор. Но в него вносить изменения сложнее, чем в проверяемую систему, — помимо связей между отдельными его частями, необходимо учитывать и связи с подсистемами м и модулями тестируемого ПО, их отдельными функциями и проверяемыми свойствами.
Наиболее значимый вклад в масштабируемость технологий создания и эксплуатации тестов вносит используемая организация тестовых наборов. Количество тестов в тестовых наборах для современного ПО довольно велико и продолжает увеличиваться. Кроме того, часто тесты связаны друг с другом разнообразными зависимостями, которые не всегда выделены явно, но должны учитываться при сопровождении тестов и внесении в них изменений. Проблемам организации сложных тестовых наборов и подходам к их решению и посвящена данная статья.
При тестировании всегда используется конечный набор тестов, даже в определении тестирования в SWEBOK [5] сказано, что оно проводится в конечном наборе ситуаций. Однако не менее важен другой аспект тестирования, также зафиксированный в этом определении, — необходимость сделать выводы о качестве проверяемой системы и потребность в том, чтобы такие выводы были достоверными. Чтобы обеспечить достоверности выводов по результатам тестирования, оно должно проводиться так, чтобы все существенные аспекты поведения тестируемой системы и все факторы, способные повлиять на его корректность, были хоть как-то затронуты. Для сложного программного обеспечения таких аспектов и факторов очень много, что и приводит и к большому количеству необходимых тестов, и к сложности их самих.
Чаще всего тестовые наборы организуются в виде комплектов тестовых вариантов. Один тестовый вариант представляет собой последовательность действий, состоящую из следующих частей.
Результаты этих действий проверяются на предмет их соответствия требованиям к тестируемой системе. Правильность остальных действий в тестовом варианте тоже часто проверяется, но проверка корректности основного набора действий является главной целью его создания.
Представление тестовой системы как набора тестовых вариантов сложилось достаточно давно, еще 30-40 лет назад. Оно довольно удобно для разработки тестов человеком — можно поставить определенную задачу (в данном случае представленную целью тестирования), а затем оформить ее решение в виде отдельной процедуры, которую можно использовать независимо от разработчика.
Кроме того, такая организация тестов имеет следующие достоинства.
Но у организации тестовой системы как набора тестовых вариантов есть и недостатки, связанные с многократно возросшей сложностью тестируемых систем и необходимостью постоянного обновления и развития наборов тестов.
Таким образом выделяются тестовые оракулы — компоненты, чья задача состоит в проверке корректности поведения тестируемой системы в ответ на воздействия определенного типа, возникающие в различных тестах. Генераторы тестовых данных тоже часто становятся отдельными компонентами, которые можно использовать в разных тестах.
Возможные виды компонентов тестовых систем обсуждаются в профиле универсального языка моделирования UML для разработки тестов (UML 2.0 Testing Profile [6-10]), в предложениях общей архитектуры инструментов тестирования, сформулированных в проекте AGEDIS [11-13], а также в работах, посвященных технологии UniTESK [14-16]. Техники выделения модулей в модульных тестах (unit tests), как общего характера, так и связанные с подходом к разработке на основе тестирования (test driven development, TDD), обсуждаются в книгах [17,18].
При автоматической генерации тестов также нужно обеспечивать уникальность каждого полученного теста, иначе тестовый набор будет содержать неизвестное количество тестов-дубликатов, не приносящих никакой пользы, но занимающих место и увеличивающих время выполнения набора. Для этого нужно либо сравнивать получаемые в итоге тестовые варианты, что часто не удобно и дает невразумительные результаты, либо организовывать дополнительные структуры данных в памяти, которые позволяют генерировать только уникальные тесты.
В силу указанных причин для сложных тестовых наборов необходимы дополнительные техники организации и введение более богатой структуры, чем выделение тестовых вариантов. Систематизировать задачи, решению которых такая структуризация должна способствовать можно следующим образом.
Эта информация должна включать в себя сведения о достигнутом тестовом покрытии по набору критериев, по которым оно измерялось, и которые хотел бы видеть пользователь. Кроме того, всегда должна быть информация о затронутых элементах тестируемой системы (вызываемые функции и методы, их классы, компоненты, подсистемы) и о проверяемых в ходе тестирования требованиях.
С ее помощью можно быстро определять тесты, которые необходимо поправить и выполнить при внесении небольших изменений в структуру или интерфейс тестируемой системы, но не в требования к ней.
Например, тест работоспособности проверяет обычно, что тестируемая функция хоть как-то работает при вызове с корректными входными данными. Найденная таким тестом ошибка обозначает, что эту функцию, скорее всего, вообще нельзя использовать, и практически всегда является критической.
При тестировании разных аспектов функциональности одна функция может вызываться с разными наборами аргументов и в различных ситуациях. Ошибка, обнаруженная одним из таких тестов, означает, что часть кода функции работает неправильно, и, в зависимости от соответствующей тестовой ситуации, эта ошибка может быть признана не существенной, т.е. подлежащей исправлению только в одной из следующих версий системы.
Сложный нагрузочный тест может обнаруживать ошибки, которые проявляются очень редко и лишь при специфических сценариях использования. Такие ошибки могут вообще не влиять на воспринимаемое пользователями качество системы.
Основные техники, используемые при структуризации сложных тестовых наборов, связаны с использованием следующих механизмов.
Наиболее простой способ внесения дополнительной структуры в тестовый набор основан на определении нескольких видов меток или квалификаторов и их использовании в коде тестов или в качестве декларативных описателей тестов.
Декларативными квалификаторами удобно пользоваться для классификации тестов по нескольким аспектам, например, по проверяемым требованиям и затрагиваемым элементам тестируемой системы. Выделить группы тестов так, чтобы они объединяли тесты по обоим этим признакам сразу, чаще всего невозможно. Поэтому лучше преобразовать тестовый набор в своего рода базу данных о тестах, где дополнительные квалификаторы буду играть роль атрибутов теста и позволят выделять подмножества тестов по практически произвольным признакам.
Помимо связей с требованиями и элементами тестируемой системы в виде квалификаторов можно представлять классификацию тестов по целям, видам проводимого тестирования, по сложности или по другим признакам.
Квалификаторы-метки обычно более удобны для выделения статической информации о тестах, однако иногда такие метки в коде тестов могут использоваться и для определения ряда характеристик теста в динамике, во время его выполнения. Например, тест может быть нацелен на достижение определенной ситуации или цели тестирования, что можно указать декларативным квалификатором, но в ходе тестирования такая ситуация не всегда возникает из-за недетерминизма поведения тестируемой системы или ошибок в ней. Чтобы уметь определять, возникала или нет эта ситуация в ходе тестирования, достаточно обеспечить сброс в трассу из кода теста определенной метки в тот момент, когда это становится ясно.
Другой вид структуризации тестовых наборов — определение и использование некоторых конфигурационных параметров, управляющих ходом тестирования, набором подключаемых компонентов и выполняемыми проверками.
Во многих случаях достаточно статически устанавливаемых конфигурационных параметров, значения которых заносятся в конфигурационные файлы или передаются запускающей тестовой набор программе в качестве аргументов.
Такие параметры могут определять глубину проводимого тестирования, набор выполняемых тестов (например, используя некоторый помечающий их квалификатор), объем проводимых проверок — некоторые проверки в тестах можно помечать как опциональные и выполнять, только если выставлено соответствующее значение некоторого параметра.
Большей гибкости управления тестовым набором можно добиться, использую динамически устанавливаемые конфигурационные параметры, хотя они несколько повышают сложность анализа и сопровождения тестового набора. Приведем два примера их использования.
При этом возникает дополнительный модуль тестовой системы, детектор конфигурации, работающий до запуска основных тестов и выявляющий текущую конфигурацию тестируемой системы.
В таких системах часто бывают функции, тщательное тестирование которых требует выполнения достаточно сложных сценариев, в которых тяжело разобраться, если возникает какая-либо ошибка. Примерами такой функциональности являются межпроцессное взаимодействие в операционных системах и зависящая от многих факторов обработка заголовков телекоммуникационных протоколов. Ошибка, связанная с полной неработоспособностью такой функции, может сделать результаты выполнения сложных тестов для нее совершенно непонятными — обычно бывает ясно, что ошибка есть, но более точная ее локализация требует значительных усилий.
Во избежание подобных затрат можно предварять выполнение сложных тестов различных аспектов такой функциональности простыми тестами на работоспособность функции в целом. Сложные тесты должны выполняться только в том случае, если предшествовавшие им простые не нашли ошибок.
Реализовать описанную процедуру можно с помощью динамически устанавливаемых по результатам простых тестов конфигурационных параметров.
Аналогично, нагрузочные тесты имеет смысл выполнять только в том случае, если проверяемые ими элементы тестируемой системы выполняют свои основные функции правильно.
Самой мощной техникой структуризации тестового набора является выделение в нем модулей, ответственных за решение разнообразных задач, возникающих во время работы теста.
Простейший способ выделения таких компонентов — определение групп тестовых вариантов, ответственных за проверку определенных элементов тестируемой системы или же некоторых аспектов требований к ней. Эти группы могут образовать иерархию, в которой группы верхнего уровня далее разбиваются на подгруппы, и т. п. При такой организации тестов выделение основных групп возможно еще на ранней стадии создания тестового набора, что позволяет эффективно распределять усилия по его разработке в большой команде.
Однако более полезным с точки зрения обеспечения многократного использования одного и того же кода является выделение модулей внутри самих тестовых вариантов.
Наиболее четко могут быть выделены следующие виды компонентов.
Компоненты-адаптеры необходимы для привязки теста к тестируемым интерфейсам, если эти интерфейсы могут меняться без изменения их функциональности или если один и тот же тест предназначен для тестирования различных систем, реализующих одни и те же функции. Адаптер реализует абстрактный интерфейс, с которым работает тест, на основе одного из реальных интерфейсов, позволяя остальным компонентам теста не зависеть от конкретного синтаксиса реальных интерфейсов.
Тестовые адаптеры — один из наиболее широко используемых видов компонентов теста. Адаптеры используются и в UniTESK под именем медиаторов [14], и при разработке тестов на TTCN для их привязки к конкретным тестируемым системам. В UML Testing Profile адаптеры не упоминаются, поскольку он определяет структуру абстрактного тестового набора, не зависящего от синтаксиса обращений к тестируемой системе.
Заглушки используются при тестировании отдельных компонентов, модулей или групп модулей, для работы которых необходимы другие компоненты, если эти другие компоненты недоступны (еще не разработаны) или просто не используются, чтобы не усложнять тестирование и анализ его результатов. Заглушка реализует интерфейс одного из отсутствующих компонентов, заменяя его в ходе теста. В качестве результатов заглушки обычно возвращают произвольные значения — постоянные или сгенерированные случайным образом. Однако иногда используются "умные заглушки" (smart stubs), реализующие какую-то часть функций заменяемого модуля или специфические сценарии его работы.
Поскольку заглушки часто возникают при модульном тестировании, в книге [18] различным видам заглушек посвящена отдельная глава. В сообществе, связанном с разработкой на основе тестирования (TDD), заглушки предпочитают называть «фиктивными объектами» (mock objects, mocks) или «тестовыми дубликатами» (test doubles). Более точно, в терминологии [18] тестовые дубликаты могут относиться к различным видам.
Роль их, как видно из названия, состоит в построении некоторого набора данных, обычно одного типа. Выгода от их использования появляется при необходимости создавать разнообразные объекты одного типа данных в разных тестах.
Генераторы тестовых данных сложной структуры обычно делаются составными. Например, генератор значений комплексных чисел можно построить из двух генераторов действительных чисел — для вещественной и для мнимой частей. Генератор сложных документов удобно строить в виде системы из взаимодействующих генераторов отдельных частей таких документов — заголовков, отдельных полей и разделов, отдельных фраз и слов.
В технологи UniTESK такого рода компоненты названы итераторами [14]. В профиле UML для разработки тестов [9] они названы селекторами данных (data selector). Селекторы могут использовать контейнеры данных (data pool), хранящие определенный набор данных, выбор из которых может производиться селектором по дополнительным правилам.
Тестовый оракул [14,19,20] — компонент, ответственный за вынесение вердикта о соответствии или несоответствии поведения системы требованиям. Работа оракула часто в большой степени зависит от конкретной тестовой ситуации, от сценария данного теста. Однако оракул для сложного сценария часто получается некоторой композицией проверок корректности его отдельных действий. Проверки корректности работы отдельных операций гораздо проще использовать многократно в разных тестах и удобнее применять для отслеживания более точной информации об ошибке, например, точного нарушенного требования или конкретной операции, выполненной с ошибкой. Поэтому удобно определить оракул типа событий или оракул операции, которые привязываются к событиям соответствующего типа или к вызовам определенной операции и выносят вердикт о том, насколько поведение системы при возникновении событий такого типа или при различных обращениях к этой операции соответствует требованиям.
Другая возможная разновидность таких оракулов — ограничения целостности данных. Они относятся к некоторому типу данных и должны проверяться каждый раз, когда данные такого типа передаются в тестируемую систему или принимаются от нее. Поскольку данные сами по себе не активны, а лишь используются в вызываемых операциях и возникающих событиях, обычно ограничения целостности данных используются всеми оракулами операций и событий, в которых затрагивается соответствующий тип данных.
Выделение таких модульных оракулов оправдано двумя факторами.
Например, при оценке корректности данных достаточно сложной структуры, таких как XML-документы или программы на языках программирования, или даже документы, в которых может быть смешано несколько языков, проводить такую проверку в рамках одного компонента крайне неудобно — он становится крайне сложным, неудобным для модификаций и сопровождения. В таком случае ограничения целостности данных разбиваются на группы ограничений, относящихся только к определенным конструкциям, и правила корректного связывания конструкций, и для каждой такой группы можно иметь отдельный компонент, проверяющий ее ограничения. Общий оракул для документа в целом получается как некоторая композиция этих компонентов.
Композиционные оракулы применяются и при тестировании распределенных систем. При этом обычно используют набор тестовых агентов, каждый из которых отслеживает поведение только одного компонента системы или небольшой их группы. Он сам может выносить вердикт о корректности событий, касающихся отслеживаемых компонентов, в том числе, используя оракулы отдельных событий. Однако к поведению системы в целом могут при этом предъявляться требования, которые ни один из таких агентов не в состоянии проверить самостоятельно. Тогда их проверка организуется в отдельном компоненте, который получает необходимую ему информацию от всех тестовых агентов.
Примером такого составного оракула является сериализатор в технологии UniTESK [15,21], который проверяет правильность набора событий в соответствии с семантикой чередования, убеждаясь, что наблюдаемое поведение системы соответствует возникновению этих событий в некотором порядке (все равно в каком). Для этого он вызывает оракулы отдельных событий в разных последовательностях, пока не будет найдена корректная или не будет показано, что подходящей последовательности нет, что означает ошибку.
Другим примером является арбитр, один из компонентов, определяемых в профиле UML для разработки тестов [9]. Однако он играет только роль посредника, позволяя тестовым агентам, выносящим свои вердикты на основе доступной им информации, обмениваться данными об этих вердиктах друг с другом.
Других компонентов тестов, которые выделялись бы в рамках различных методов построения тестов и различными авторами, пока не удается найти. Все остальные виды компонентов специфичны для определенных методов тестирования и чаще всего не имеют аналогов в других подходах.
При определении модульной структуры тестов стоит учитывать, что вместе с появлением возможности многократно использовать одни и те же компоненты, повышается и их сложность для людей, не знающих об используемой архитектуре. Поэтому архитектура таких тестов, с указанием всех видов используемых компонентов и задач, решаемых ими, должна быть описана в документации на тестовый набор. Крайне желательно постепенно стандартизовать достаточно широкий набор видов модулей теста, чтобы сделать возможным их использование в различных инструментах.
Другое препятствие к широкому использованию модульности тестов связано с усложнением анализа ошибок. Тест уже не представляет собой единый, изолированный сценарий работы, для понимания которого не нужно заглядывать в много различных документов или в код различных компонентов. Поток управления в рамках одного тестового варианта или более сложного теста становится иногда очень причудливым и сложным, разобраться в нем становится тем труднее, чем больше разных видов компонентов используется. Поэтому при использовании модульных тестов необходимы дополнительные усилия по упрощению анализа ошибок. Одним из вариантов решения этой проблемы может быть автоматическое создание более простых, классических тестовых вариантов, повторяющих ситуацию, в которой обнаружена ошибка.
В статье рассмотрен ряд проблем, делающих необходимой более тонкую структуризацию сложных тестовых наборов, чем традиционное разбиение на тестовые варианты. Также рассказывается об основных техниках внесения дополнительной структуры в тестовый набор: выделении модулей, использовании квалификаторов и определении набора конфигурационных параметров.
Наиболее легко с широко применяемыми сейчас подходами к разработке тестов сочетается использование набора квалификаторов для классификации тестов и указания связей между тестами и требованиями или элементами тестируемой системы. Однако большинство инструментов управления тестами поддерживает использование только предопределенного набора квалификаторов, которые не могут быть расширены. Пока только HP/Mercury TestDirector [22] имеет возможность добавления пользовательских квалификаторов, которые можно затем использовать для построения специфических отчетов — группировки или отбрасыванию результатов тестов по значениям квалификатора.
Более сложно с имеющимися подходами к управлению тестами интегрируются использование системы конфигурационных параметров, а также выделение разнообразных модулей и их многократное использование в тестах. Конфигурационные параметры можно использовать только в виде дополнительных квалификаторов, что далеко не всегда удобно, особенно для динамически устанавливаемых параметров.
Проблема выделения различных модулей в сложных тестах стоит наиболее остро. Только в последние 5 лет появилось несколько подходов [6,11,14,18] к разработке тестов, уделяющих этому аспекту достаточно внимания. В то же время, общим элементом этих подходов, да и то не всех, можно считать выделение заглушек, адаптеров, генераторов тестовых данных и некоторых общесистемных компонентов тестов. Выделение более разнообразного набора модулей, включающего оракулы отельных операций и событий, с одной стороны, является необходимым для повышения управляемости и гибкости современных сложных тестовых наборов, но с другой стороны, усложняет ряд традиционных видов деятельности — конфигурирование тестового набора и анализ обнаруживаемых им ошибок. Для разрешения возникающих проблем необходимо разработать новые техники решения соответствующих задач в модульных тестовых наборах.