Тестирование софта - статьи

             

Аннотация


В статье описывается подход к построению инфраструктуры использования программных стандартов. Предлагаемый подход основан на формализации стандартов и автоматизации построения тестов для проверки соответствия им из полученных формальных спецификаций. В рамках этого подхода предлагается технологическая поддержка для решения ряда возникающих инженерных и организационных задач, что позволяет использовать его для сложных промышленных стандартов программного обеспечения. Этот тезис иллюстрируется использованием описанного подхода для формализации ядра Базового стандарта Linux (Linux Standard Base). Данная работа лежит в рамках подходов к решению задачи по обеспечению развития надежных крупномасштабных программных систем, провозглашенной международным академическим сообществом одним из Больших Вызовов в информатике [,].

Буфер трансляции адресов


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

В общих словах трансляция виртуального адреса с помощью TLB осуществляется следующим образом. Если буфер содержит ячейку с нужным номером виртуальной страницы, в определенном выходном регистре модуля формируется соответствующий физический адрес — номер виртуальной страницы меняется на номер физической, а смещение остается прежним; в противном случае, на одном из выходов модуля устанавливается сигнал, говорящий о промахе в буфер (в этом случае микропроцессор генерирует исключение).

Другие подходы к построению тестов на соответствие стандартам


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

Этот подход имеет много общего с представленным в данной статье. Различия связаны, в основном, с необходимостью иметь дело со стандартами большего объема, такими как стандарты POSIX или LSB на интерфейсы библиотек операционных систем. Большой размер стандартов и соответствующих спецификаций приводит к практической невозможности использовать как полные тестовые наборы в терминах работы [], поскольку они бесконечны, так и выбор набора целей тестирования "на глаз", который является одним из традиционных шагов разработки тестов для телекоммуникационных протоколов. Вместо этого используется более систематическое выделение отдельных требований, понятие критерия тестового покрытия, выбор критерия, ориентированного на учет всех выявленных требований, и генерация тестов, нацеленная на достижение высоких показателей покрытия по выбранному критерию.

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

Статья [] представляет другую попытку формализации стандарта на примере ШЕЕ 1003.5 - POSIX Ada Language Interfaces (интерфейсы POSIX для языка Ada). В рамках описываемого в ней метода на базе требований стандарта сразу строились формальные описания проверяющих их тестов, без промежуточных спецификаций самих требований.
Такой метод кажется нам принципиально мало отличающимся от традиционной ручной разработки тестов, при которой разработчик теста читает стандарт, придумывает на основе прочитанного тесты и записывает их в виде некоторых программ. Существует ряд аналогичных работ по разработке тестовых наборов для проверки соответствия стандартам интерфейсов операционных систем. Наиболее известные стандарты в этой области это IEEE Std 1003.1, или POSIX [], и Базовый стандарт Linux, или LSB []. Имеются и наборы тестов на соответствие этим стандартам - это сертификационные тесты POSIX от Open Group [], открытый проект Open POSIX Test Suite [], и официальные сертификационные тесты на соответствие LSB [] от Free Standards Group []. Все эти проекты используют схожие технологии выделения требований из текста стандарта и создания соответствующего каталога требований. После этого тесты разрабатываются вручную на языке С с применением подхода "один тест на одно требование". Они не используют формализацию требований и автоматическую генерацию тестов. Стоит отметить, что использование подхода "один тест на одно требование" создает предпосылки для укрупнения требований при их выделении, так как велик соблазн проверить как можно больше в одном тесте. К примеру, в тестах Open POSIX мы обнаружили требования, которые включают в себя десяток утверждений, которые в проекте Центра верификации ОС Linux выделяются в отдельные требования. Такое укрупнение требований может приводить к тому, что некоторые утверждения стандарта упускаются из виду и не проверяются, в то время как крупное интегральное требование, в которое они входят, считается протестированным. В результате построенный на основе таких требований отчет об их покрытии может искажать реальное положение дел, утверждая, что все требования стандарта проверены. Кроме того, автоматическая генерация тестов из спецификаций, используемая в нашем подходе, делает тестовый набор более управляемым, позволяя описывать сами требования стандарта в одном месте, а технику, используемую для их проверки, и тестовые данные - в другом.Это значительно облегчает внесение изменений в тесты при развитии стандарта или их адаптации под требования специфической предметной области.

Формализация стандартов


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

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

Формализация стандарта включает несколько видов деятельности. Декомпозиция стандарта. Так как количество интерфейсных элементов, описанных в стандарте, может быть очень велико, то на первом шаге они разбиваются на логически связанные группы. Такая группа обычно состоит из операций и типов данных, связанных с определенным набором близких функций, и замкнута относительно обращения операций. Например, операции открытия и закрытия файлов, создания и удаления объектов нужно помещать в одну группу. Вся дальнейшая работа производится в рамках таких групп. Выделение требований. Следующая задача заключается в выделении всех требований стандарта к описываемым им элементам интерфейса, которые могут быть проверены. Так как одна и та же вещь может быть описана в нескольких местах, то все соответствующие разделы стандарта необходимо внимательно, фраза за фразой, прочитать и пометить все найденные требования и ограничения. Затем выделенные требования объединяются в непротиворечивый набор.
Эта работа достаточно утомительная и тяжелая, но ни в коем случае не механическая. Ее сложность связана с преобразованием неформальных требований и ограничений в формальные, а, по словам М. Р. Шуры-Буры, переход от неформального к формальному существенно неформален. Утверждения и ограничения из разных частей стандарта могут быть несовместимыми, двусмысленными, представлять похожие, но не одинаковые идеи. Только текста стандарта часто не достаточно, чтобы прийти к непротиворечивой полной картине. Поэтому при выделении требований необходимо пользоваться общими знаниями о предметной области, книгами и статьями по тематике, знанием о используемых в существующих системах решениях, а также общаться с авторами стандарта, экспертами в области и опытными разработчиками. Некоторые аспекты специально не определяются стандартом точно, для того чтобы реализации различных производителей соответствовали ему. Занятный пример можно найти в текущей версии стандарта POSIX []. Описание функций fstatvfs() и statvfs() из sys/statvfs.h говорит: "Не специфицировано, имеют ли все члены структуры statvfs смысл для всех файловых систем" ("It is unspecified whether all members of the statvfs structure have meaningful values on all file systems"). Существует два возможных пути разрешения подобных ситуаций. Можно ничего не проверять. В этом случае никаких требований из соответствующей части стандарта не извлекается. Если существует небольшое количество возможных реализаций, то требования к ним могут быть представлены в виде параметризированных ограничений. При этом вводится конфигурационный параметр, разные значения которого соответствуют различным возможным реализациям и определяют конечный вид проверяемого требования. Пример из описания функции basename() в стандарте POSIX: "Если строка, на которую указывает параметр path, равна "//", то реализация может возвращать как '/', так и "//"" ("If the string pointed to by path is exactly "//", it is implementation-defined whether '/' or "//" is returned"). Выделение требований проводится до тех пор, пока весь текст стандарта, касающийся выбранной группы элементов интерфейса, не будет разбит на требования, которые можно проверить, и остальные фразы, не содержащие проверяемых утверждений.





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

Разработка спецификаций. Эта работа обычно выполняется параллельно и вперемешку с предыдущей. Они разделены только в целях ясности изложения. Большинство найденных требований записываются в форме контрактных спецификаций операций, типов данных и элементов данных. Каждая операция описывается с помощью предусловия и постусловия. Предусловие операции определяет область ее определения. Постусловие определяет ограничения на результаты работы операции и итоговое состояние системы в зависимости от значений ее входных параметров и состояния системы до ее вызова. Для типов данных и элементов данных определяются ограничения целостности, называемые инвариантами. Те требования, которые не оформляются в виде контрактных спецификаций, делятся на следующие группы. Непроверяемые требования. Часть требований не проверяется, потому что их вообще нельзя проверить с помощью конечной процедуры тестирования или потому что не существует методов надежной проверки таких требований (например, "функция должна выдавать результат за время, меньшее, чем любой многочлен второй степени от значения ее параметра"), а также из-за крайней неэффективности их проверки ("возвращаемый функцией указатель должен отличаться от указателя на любой объект, присутствовавший в системе до ее вызова"). Проверяемые требования. Другая часть требований проверяется не в спецификациях по соображениям эффективности тестов и удобства их модификации.


Ряд требований, касающихся целостности данных некоторых типов, может быть отражен в самом описании их структуры в спецификациях. При этом проверка соответствующих ограничений происходит при преобразовании из данных реализации в модельные данные. Если эти ограничения нарушаются, такое преобразование становится невозможным и спецификация не может с ними работать, но сообщение об их проверке и обнаруженном несоответствии появится в трассе теста. Некоторые требования можно проконтролировать только в результате многократных обращений к тестируемой функции и проверки каких-то свойств всей совокупности полученных при этом результатов. Эти требования часто неудобно оформлять в виде спецификаций - для них создается отдельный тестовый сценарий (см. ниже), который производит все нужные обращения и проверяет необходимые свойства полученных результатов. Примером такой ситуации может служить требование к функции rand() генерировать при многократных обращениях последовательность случайных чисел, равномерно распределенных на некотором интервале. Для проверки этого свойства можно накопить данные о результатах ее вызовов и применить к их совокупности, например, критерий Колмогорова. Эта деятельность имеет два результата. Формальные спецификации всех элементов интерфейса. Код спецификаций размечается, чтобы указать связь между описанными формальными ограничениями и соответствующими им требованиями из каталога требований. Конфигурационная система стандарта. Эта система состоит из набора конфигурационных параметров, как объявленных в стандарте, так и дополнительно введенных разработчиками спецификаций, зависимостей между ними, а также связей между ними, элементами интерфейса и проверяемыми ограничениями. Некоторые конфигурационные опции, представленные как значения параметров, управляют набором проверяемых требований. Другие могут говорить о том, что определенная функциональность вообще отсутствует в системе, и соответствующие операции не должны вызываться. Третьи могут влиять на возможные коды ошибок, возвращаемые операциями. Пример первого случая можно увидеть в описании функции pthread_create() из стандарта POSIX: "Если макрос _POSIX_THREAD_CPUTIME определен, то новый поток должен иметь доступ к часам процессорного времени, и начальное значение этих часов должно быть выставлено в ноль" ("If _POSIX_THREAD_CPUTIME is defined, the new thread shall have a CPU-time clock accessible, and the initial value of this clock shall be set to zero"). Определение критериев покрытия. Последний вид деятельности в рамках формализации стандарта заключается в определении критериев покрытия, которые могут быть использованы для измерения адекватности тестирования реализации на соответствие стандарту.Базовый критерий - это необходимость покрытия всех требований стандарта, применимых к текущей конфигурации тестируемой системы. Более строгие критерии могут определять дополнительные ситуации для тестирования. Все эти критерии базируются на спецификациях, представляющих требования стандарта. Критерии покрытия сложным образом связаны с конфигурационными параметрами. Возможность покрытия определенной ситуации может зависеть от текущих значений конфигурационных параметров, и эта зависимость должна быть зафиксирована, чтобы избежать многочисленных трудностей при анализе результатов тестирования. Результатом этой работы является набор критериев покрытия, тесно связанный с конфигурационной системой. Процесс формализации стандарта и последующей разработки тестов на основе ее результатов проиллюстрирован на .

Формальная спецификация подсистемы управления памятью


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

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

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

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

Генерация тестовых программ для подсистемы управления памятью микропроцессора


,
Труды Института системного программирования РАН

Использование формальных методов для обеспечения соблюдения программных стандартов


, , , , ,

Препринт Института системного программирования РАН (ИСП РАН)

Используемый метод генерации тестовых программ


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

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

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

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

Таким образом, структуру тестовой программы можно описать с помощью формулы test = {⟨initi, actioni, posti⟩}i=1,n, где initi — это инициализирующие инструкции, actioni — тестовое воздействие, posti — тестовый оракул.

Ниже приведен фрагмент тестовой программы для микропроцессора с системой команд MIPS64 , который включает один тестовый вариант. /////////////// Инициализирующие инструкции /////////////// // Инициализация регистров инструкции 1: // загрузка виртуального адреса в базовый регистр a0 // a0[base]=0xffffffff81af1590 ori a0, zero, 0xffff dsll a0, a0, 16 ori a0, a0, 0xffff dsll a0, a0, 16 ori a0, a0, 0xa1af dsll a0, a0, 16 ori a0, a0, 0x1590 // Инициализация памяти для инструкции 1 ori a1, zero, 0xdead sw a1, 0(a0) // Инициализация регистров инструкции 2: // загрузка виртуального адреса в базовый регистр t1 // t1[base]=0xffffffff81c49598 ... // Инициализация памяти для инструкции 2 ... // Инициализация кэша L1 для инструкции 1: // помещение данных в кэш L1 lw t0, 0(a0) // Инициализация кэша L1 для инструкции 2: // вытеснение данных из кэша L1 ... ////////////////// Тестовое воздействие /////////////////// // Зависимость между инструкциями 1 и 2: L1IndexEqual=false lw v0, 0(a0) // L1Hit=true sw a2, 0(t1) // L1Hit=false ///////////////////// Тестовый оракул ///////////////////// // Тестовый оракул для инструкции 1 ori t1, zero, 0xdead // Ошибочное завершение при несоответствии результата bne v0, t1, error_found nop // Тестовый оракул для инструкции 2 ...

Тестовое воздействие состоит из двух инструкций: lw, осуществляющей загрузку слова, и sw, которая сохраняет слово по указанному адресу. Для наглядности тестовые ситуации для этих инструкций затрагивают только кэш-память первого уровня (L1): первая инструкция вызывает попадание в кэш, вторая — промах.

Несколько слов о вспомогательных инструкциях, используемых в примере. Инструкция ori осуществляет побитовое ИЛИ значения второго регистра с 16 битным значением, заданного в третьем операнде, результат записывается в первый регистр; инструкция dsll сдвигает значение второго регистра влево на заданное число разрядов и сохраняет результат в первом регистре; bne — это инструкция условного перехода (переход осуществляется в случае неравенства значений регистров).

Кэш-память


Кэш-память представляет собой промежуточный буфер с быстрым доступом, содержащий наиболее часто используемые данные, которые хранятся в менее быстродействующих устройствах. Кэш-память современных микропроцессоров имеет несколько уровней (обычно их два — L1 и L2). Кэш Li буферизует обращения к кэшу Li+1; кэш последнего уровня является самым крупным, и данные в него подгружаются непосредственно из оперативной памяти.

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

В общем случае кэш-память состоит из некоторого числа множеств, обозначим это число S = 2s; каждое множество состоит из E строк; а каждая строка состоит из блока данных размера B = 2b, тэга — старших разрядов физического адреса, используемых для вычисления признака попадания, — и бит управляющей информации. В зависимости от значений S и E различают три типа кэш-памяти: кэш-память прямого отображения (E = 1); полностью ассоциативная кэш-память (S = 1); частично ассоциативная кэш-память (E > 1 и S > 1).

Величина E называется уровнем ассоциативности кэш-памяти.

Пусть физический адрес имеет разрядность m. В большинстве случаев при обращении к кэш-памяти с параметрами ⟨S = 2s, B = 2b, E⟩ физический адрес интерпретируется следующим образом: биты [0, …, b-1] определяют позицию байта внутри блока данных, [b, …, b+s–1] — номер множества и, наконец, [b+s, …, m] — тэг. Для определения, имеет место попадание или промах, по физическому адресу вычисляется номер множества, после чего для каждой строки этого множества осуществляется сравнение тэга адреса, по которому осуществляется обращение, с тэгом, хранящимся в строке. Если для одной из строк сравнение истинно, значит, имеет место кэш-попадание, и в блоке данных этой строки на соответствующей позиции находятся требуемые данные.

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

Литература


. А.С. Камкин. Генерация тестовых программ для микропроцессоров. Труды Института системного программирования РАН, 2008. MIPS64™ Architecture For Programmers. Revision 2.0. MIPS Technologies Inc., June 9, 2003. K.J. Hayhurst, D.S. Veerhusen, J.J. Chilenski, and L.K. Rierson. A Practical Tutorial on Modified Condition/Decision Coverage. Report NASA/TM-2001-210876, NASA, USA, May 2001. A. Adir, R. Emek, Y. Katz, A. Koyfman. DeepTrans – A Model-Based Approach to Functional Verification of Address Translation Mechanisms. Microprocessor Test and Verification: Common Challenges and Solutions, 2003. . A. Adir, E. Almog, L. Fournier, E. Marcus, M. Rimon, M. Vinov, A. Ziv. Genesys-Pro: Innovations in Test Program Generation for Functional Processor Verification. Design and Test, 2004. M. Behm, J. Ludden, Y. Lichtenstein, M. Rimon, M. Vinov. Industrial Experience with Test Generation Languages for Processor Verification. Design Automation Conference, 2004.

Обычно функциональное тестирование проводится для модели микропроцессора, разработанной на языке описания аппаратуры (Verilog, VHDL).

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

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

[1]http://www.fmnet.info/gc6/
[2]Tony Hoare and Robin Milner, eds. Grand Challenges in Computing. Research.
http://www.ukcrc.org.uk/gcresearch.pdf
[3]ISO 9646. Information Theory - Open System Interconnection - Conformance Testing Methodology and Framework. ISO, Geneve, 1991.
[4]ITU-T. Recommendation Z.500. Framework on formal methods in conformance testing.
International Telecommunications Union, Geneve, Switzerland, 1997.
[5]http://www.linuxbase.org/spec
[6]http://www.unix.org/version3/ieee_std.html
[7]G. Bernot. Testing against Formal Specifications: A Theoretical View. In Proc. of TAPSOFT'91, Vol. 2. S. Abramsky and T. S. E. Maibaum, eds. LNCS 494, pp. 99-119, Springer-Verlag, 1991.
[8]E. Brinksma, R. Alderden, R. Langerak, J. van de Lagemaat, and J. Tretmans. A formal approach to conformance testing. In J. de Meer, L. Mackert, and W. Effelsberg, eds. 2-nd Int. Workshop on Protocol Test Systems, pp. 349-363. North-Holland, 1990.
[9]J. Tretmans. A Formal Approach to Conformance Testing. PhD thesis, University of Twente, Enschede, The Netherlands, 1992.
[10]I. Bourdonov, A. Kossatchev, V. Kuliamin, and A. Petrenko. UniTesK Test Suite Architecture. In Proc. ofFME2002. LNCS 2391, pp. 77-88, Springer-Verlag, 2002.
[11]V. Kuliamin, A. Petrenko, N. Pakoulin, A. Kossatchev, I. Bourdonov. Integration of Functional and Timed Testing of Real-time and Concurrent Systems. In Proc. of PSI 2003, LNCS 2890, pp. 450-461, Springer-Verlag, 2003.
[12]В. В. Кулямин, А. К. Петренко, А. С. Косачев, И. Б. Бурдонов. Подход UniTesK к разработке тестов. Программирование, 29(6):25-43, 2003.
[13]V. Kuliamin. Model Based Testing of Large-scale Software: How Can Simple Models Help to Test Complex System. In Proc. of 1-st International Symposium on Leveraging Applications of Formal Methods, Cyprus, October 2004, pp. 311-316.
[14]V. Kuliamin, A. Petrenko, N. Pakoulin. Practical Approach to Specification and Conformance Testing of Distributed Network Applications. In M. Malek, E. Nett, N. Suri, eds. Service Availability. LNCS 3694, pp. 68-83, Springer-Verlag, 2005.
[15]http://www.linuxtesting.ru
[16]http://www.opengroup.org/bookstore/catalog/c610.htm
[17]http://www.caldera.com/developers/devspecs/
[18]ISO/IEC9899. Programming Languages -С ISO, Geneve, 1999.
[19]J. F. Leathrum and K. A. Liburdy. A Formal Approach to Requirements Based Testing in Open Systems Standards. In Proc. of 2-d International Conference on Requirements Engineering, 1996, pp. 94-100.
[20]http://www.opengroup.org/testing/testsuites/TestSuiteIndex.htm
[21]http://posixtest. sourceforge.net/
[22]http://www.linuxbase.org/download/#test_suites
[23]http://fireestandards.org/
[24]http://www.osdl.org/lab_activities/carrier_grade_linux



Подготовка тестового воздействия


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

Для инструкций, работающих с памятью, подготовка тестовых ситуаций состоит из двух основных частей: инициализация буфера трансляции адресов и инициализация кэш-памяти. Инструкции подготовки буфера трансляции адресов могут изменить состояние кэш-памяти, поэтому при построении программы подготовки тестового воздействия целесообразно сначала осуществлять подготовку буфера трансляции адресов для всех инструкций тестового воздействия, а затем — подготовку кэш-памяти (подготовка кэш-памяти не затрагивает TLB, если использовать адреса из неотображаемых сегментов адресного пространства).

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

Подсистема управления памятью


Подсистемой управления памятью (MMU, Memory Management Unit) называется подсистема компьютера, отвечающая за обработку обращений к памяти. К ее функциям относятся: трансляция виртуальных адресов в физические, защита памяти, управление кэш-памятью и др. . Вообще говоря, отдельные компоненты подсистемы управления памятью могут располагаться вне интегральной схемы микропроцессора, однако в данной статье рассматриваются только те составляющие MMU, которые являются частью микропроцессора: буфер трансляции адресов (TLB, Translation Lookaside Buffer) и устройства кэш-памяти.



Практическая апробация подхода


Мы применили описанный подход для тестирования подсистемы управления памятью MIPS64-совместимого микропроцессора. В качестве тестовых воздействий использовались всевозможные пары, составленные из четырех инструкций: lb (загрузка байта), ld (загрузка двойного слова), sb (сохранение байта) и sd (сохранение двойного слова). Структура используемых тестовых ситуаций и зависимостей была близка к описанной в статье, но были дополнительно учтены управляющие биты в TLB и наличие микро-TLB — промежуточного буфера, содержащего записи, по которым были последние обращения.

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

Применения описанного подхода


Представленный выше подход был успешно использован для формализации и создания наборов тестов для частей стандартов на протоколы IPv6 и IPMP2 [,].

Более масштабным применением этого подхода стал проект Центра верификации ОС Linux [] по формализации Базового стандарта Linux (Linux Standard Base, LSB) [] и разработке тестового набора для проверки соответствия ему. Цель первой фазы проекта - создать формальные спецификации и соответствующий тестовый набор для 1532 функций основных библиотек Linux, перечисленных в разделах III и IV стандарта LSB 3.1 (они описывают базовые библиотеки и библиотеки утилит).

Стандарт LSB задает требования ко многим из этих функций, ссылаясь на уже существующие стандарты. Непосредственно в LSB описывается лишь 15% интерфейсов, большинство - 60% функций - определяются в стандарте Single UNIX Specification [], который включает текущую версию POSIX, а остальные - в таких спецификациях как X/Open Curses [], System V Interface Definition [] и ISO/IEC 9899 (стандарт языка С) []. LSB не включает в себя все эти стандарты, а ссылается лишь на их части, касающиеся описания требований для отдельных функций.

Первая фаза проекта завершится в конце 2006 года. К этому времени планируется сделать все ее результаты доступными в виде открытого кода. В составе этих результатов будет следующее. Дополнения к стандарту LSB Core 3.1 в виде формальных спецификаций его требований на расширении языка С (SeC - Specification Extension of С). Такие спецификации выражают требования стандарта в формальном виде, позволяя прояснить нечеткие места в его тексте и сделать явными все подразумеваемые в нем ограничения. Они послужат основой для разработки тестового набора для проверки соответствия стандарту LSB. Список замечаний к текстам LSB и связанных стандартов, наработанных в ходе формализации. Это указания на нечеткие, двусмысленные, противоречивые или ошибочные места в стандартах. Они будут направляться разработчикам стандартов для внесения изменений в их будущие версии. Тестовый набор для проверки соответствия поведения системных интерфейсов конкретной реализации Linux требованиям стандарта LSB Core 3.1.
Он будет представлять собой набор программ, которые в результате своего выполнения строят HTML-отчеты о проверенных требованиях и найденных несоответствиях.
Тестовый набор будет иметь систему конфигурации, которая позволитнастраивать тесты на особенности конкретной тестируемой системы, допускаемые стандартом, а также установить параметры, определяющие состав тестируемых модулей и глубину тестирования. В начале проекта 1532 функции LSB были разбиты на 147 групп функционально связанных операций, которые в свою очередь группируются в большие подсистемы в соответствии с общей функциональностью - потоки, межпроцессное взаимодействие, файловая система, управление динамической памятью, математические функции, и т.д. За первые 3 месяца работы были проспецифицированы и снабжены базовыми тестовыми сценариями 320 функций из 41 группы. В процессе формализации было выделено около 2150 отдельных требований стандарта к этим функций. При этом к некоторым функциям предъявляется всего лишь несколько требований, в то время как к другим - несколько десятков. На основе достигнутой производительности можно утверждать, что первая фаза проекта потребует около 15 человеко-лет. Чтобы начать работать в этом проекте, опытному разработчику программного обеспечения (не обычному тестировщику!) требуется около месяца для обучения особенностям технологии и изучения сопутствующих процессов. Полученные предварительные результаты позволяют надеяться, что рассмотренный подход может успешно применяться для проектов такого масштаба.

Разработка тестов на соответствие стандарту


В основе процесса разработки тестов на соответствие стандарту лежит технология UniTesK, разработанная в ИСП РАН на базе многолетнего опыта проектов по тестированию сложного промышленного ПО. Эта технология использует подход к формальному тестированию, разработанный в работах Верно (Bernot) [], Бринксмы (Brinksma) и Тритманса (Tretmans) [,] и описанный в телекоммуникационных стандартах ISO 9646 [] и ITU-T Z.500 []. Основные положения этого подхода можно сформулировать следующим образом ( иллюстрирует связи между упоминаемыми ниже понятиями). Требования стандарта представлены в виде модели, описанной с помощью некоторого формализма. Эта модель называется спецификацией. Предполагается, что программная система, чьё соответствие стандарту мы пытаемся установить - тестируемая система, - может быть адекватно смоделирована с помощью этого же формализма. Соответствующую нашей системе модель мы называем реализацией. Мы не знаем её деталей, но предполагаем, что она существует. Адекватность моделирования в данном случае означает, что мы не можем наблюдать никаких различий между реальным поведением тестируемой системы и модельным поведением реализации. Тот факт, что тестовая система соответствует стандарту, моделируется посредством отношения соответствия (conformance relation, implementation relation) между реализацией и спецификацией. Модельные тесты строятся на основе спецификации или извлекаются из нее.

Модельный тест - это модель в том же самом формализме, способная взаимодействовать с другими моделями и выдающая булевский вердикт как результат этого взаимодействия. Реализация проходит модельный тест, если он выдаёт вердикт "истина" в результате взаимодействия с ней. Имеет смысл строить только значимые тесты, т.е. те, что проходятся любой реализацией, соответствующей спецификации. Тест, не являющийся значимым, может отвергнуть совершенно корректную реализацию. Результатом извлечения тестов является набор модельных тестов или модельный тестовый набор. Желательно, чтобы он был полным тестовым набором - таким, что реализация проходит все тесты из него тогда и только тогда, когда она соответствует спецификации. Модельные тесты транслируются в тестовые программы, взаимодействующие с тестируемой системой.
Примерами таких критериев являются покрытие ветвей в коде постусловия и покрытие дизъюнктов из дизъюнктивной нормальной формы условий этих же ветвлений. Есть возможность вводить произвольные критерии, описывая дополнительные ситуации, в которых работу системы необходимо проверить. Тестовый сценарий создается для достижения определённого покрытия заданной группы операций. Такой сценарий определяет конечный автомат, моделирующий поведение тестируемого компонента таким образом, что выполнение всех его переходов гарантирует покрытие 100% ситуаций в соответствии с выбранным критерием. Автомат задается при помощи функции вычисления состояния и набора допустимых действий в произвольном состоянии. Этот набор действий зависит от данных состояния. Каждое действие соответствует вызову одной из операций с некоторыми аргументами. Сами тесты или последовательности тестовых воздействий генерируются при выполнении тестового сценария, во время которого автоматически строится некоторый путь, покрывающий все переходы описываемого им автомата. Построение тестового набора для проверки соответствия практически используемым стандартам ПО приводит к необходимости разработки конфигурационной системы тестов. Эта система включает конфигурационную систему стандарта (см. выше) и дополнительные параметры, управляющие работой тестов. Разные значения этих параметров соответствуют различным критериям покрытия, разным наборам тестовых сценариев и т.п. Хорошая конфигурационная система делает тестовый набор применимым в любых условиях, где может функционировать реализация рассматриваемого стандарта.

Тестирование софта - статьи


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

Сравнение с существующими подходами


Нам известен только один подход к автоматизированному построению тестовых программ для MMU, использующий формальные спецификации подсистемы, — DeepTrans, разработка исследовательской лаборатории IBM в г. Хайфе (IBM Haifa Research Lab) . Эта методика нацелена на тестирование части подсистемы, отвечающей за преобразование виртуальных адресов.

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

Подход DeepTrans предполагает ручную разработку шаблонов тестовых программ, но в шаблонах можно указывать тестовые ситуации, связанные с трансляцией адресов, извлеченные из спецификации. На основе шаблонов генератор Genesys-Pro путем разрешения ограничений строит набор тестовых программ [6, 7, 8].

Сравнение DeepTrans (в паре с генератором Genesys-Pro) с используемым нами подходом (MicroTESK) приводится в таблице 1.

Таблица 1. Сравнение DeepTrans с MicroTESK

Сравниваемая характеристика DeepTrans (Genesys-Pro) MicroTESK
Поддержка пользовательских шаблонов тестовых программ Genesys-Pro предоставляет развитый язык описания шаблонов тестовых программ MicroTESK позволяет описывать лишь сравнительно простые шаблоны тестовых программ
Поддержка автоматической генерации тестовых шаблонов Автоматическая генерация тестовых шаблонов не поддерживается Поддерживается автоматическая генерация несложных тестовых шаблонов
Поддержка описания тестовых ситуаций Поддерживается Поддерживается
Поддержка описания зависимостей по адресам Специальной поддержки нет, зависимости вручную описываются в тестовом шаблоне Поддерживается описание сложных зависимостей по адресам
Поддержка моделирования подсистемы управления памятью DeepTrans предоставляет специальный декларативный язык описания механизмов трансляции В MicroTESK реализована библиотека классов на языке Java, моделирующая устройства MMU
Поддержка генерации самопроверяющих тестов Поддерживается Поддерживается

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

Тестирование подсистемы управления памятью


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

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

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

Типы тестовых ситуаций для подсистемы управления памятью


Под тестовой ситуацией для инструкции понимается ограничение на значения входных операндов и состояние микропроцессора перед началом выполнения инструкции. Множество возможных тестовых ситуаций выявляется на основе анализа описания архитектуры микропроцессора.

При тестировании MMU структура тестовых ситуаций определяется функциональностью подсистемы, связанной с обработкой запросов к памяти. Например, на первом этапе обработки определяется сегмент адресного пространства, к которому относится виртуальный адрес: если сегмент является отображаемым, трансляция адреса осуществляется с помощью TLB; в противном случае, вычисление физического адреса производится в обход буфера трансляции адресов. Для данного этапа определены две тестовые ситуации: Mapped = true и Mapped = false. Далее, если виртуальный адрес является отображаемым, возможны две альтернативные ситуации: TLBHit = true, когда страница виртуальной памяти находится в TLB (попадание), и TLBHit = false, когда страницы в буфере нет (промах). Выделение ситуаций можно продолжить до тех пор, пока данные из памяти не будут загружены в регистр или, наоборот, сохранены из регистра в память (в зависимости от операции).

Для подсистемы управления памятью можно выделить следующие типы элементарных ситуаций (ситуаций для отдельных этапов обработки), на основе композиций которых строятся результирующие тестовые ситуации: Mapped — отображаемый/неотображаемый сегмент виртуальной памяти; Cached — кэшируемый/некэшируемый сегмент виртуальной памяти; TLBHit — попадание/промах в TLB; Valid — бит достоверности секции TLB; L1Hit — попадание/промах в кэш-память L1; L2Hit — попадание/промах в кэш-память L2.

На рисунке 1 элементарные ситуации структурированы в виде дерева (пунктирная дуга соответствует значению false, сплошная — значению true). Результирующая тестовая ситуация для инструкции соответствует пути от корня до листовой вершины.


Рисунок 1. Иерархии тестовых ситуаций

Отметим, что выделение тестовых ситуаций можно проводить на разных уровнях абстракции, например, тестовую ситуацию, связанную с промахом или попаданием в буфер трансляции адресов в MIPS64-совместимом микропроцессоре, можно детализировать на основе выражения, вычисляющего признак попадания в строку TLB : (TLB[i]R = va63..62) and
((TLB[i]VPN2 and not (TLB[i]Mask)) = (vaSEGBITS-1..13 and not (TLB[i]Mask))) and
(TLB[i]G or (TLB[i]ASID = EntryHiASID))

Уровень детализации может быть разным. При тщательном тестировании рассматриваются все комбинации значений истинности элементарных условий (exclusive condition coverage) или только те комбинации, когда значение истинности одного условия влияет на значение истинности всего выражения (MC/DC, Modified Condition/Decision Coverage) .

Типы зависимостей для подсистемы управления памятью


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

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

Мы используем следующие типы зависимостей для буферов, входящих в состав MMU: IndexEqual — совпадение используемых множеств; EntryEqual — совпадение строк внутри множества; BlockEqual — совпадение данных внутри строки; EntryReplace — обращение к данным, строка с которыми была вытеснена из буфера предшествующей инструкцией; BlockEqual — совпадение данных внутри строки.

В целом для подсистемы управления памятью можно выделить следующие типы элементарных зависимостей: VAEqual — совпадение/несовпадение виртуальных адресов; VPNEqual — совпадение/несовпадение номеров страниц виртуальной памяти; PAEqual — совпадение/несовпадение физических адресов; PFNEqual — совпадение/несовпадение номеров страниц физической памяти; L1IndexEqual — совпадение/несовпадение множеств кэш-памяти L1; L2IndexEqual — совпадение/несовпадение множеств кэш-памяти L2; L1Replace — совпадение/несовпадение тэга кэш-памяти L1 с тэгом данных, которые были вытеснены из кэш-памяти предшествующей инструкцией; L2Replace — совпадение/несовпадение тэга кэш-памяти L2 с тэгом данных, которые были вытеснен из кэш-памяти предшествующей инструкцией.

В силу иерархической организации памяти зависимости по адресам имеют многоуровневую структуру. Фрагмент дерева возможных зависимостей представлен на рисунке 2 (пунктирная дуга соответствует значению false, сплошная — значению true). Результирующая зависимость между инструкциями соответствует пути от корня до листовой вершины.


Рисунок 2. Фрагмент иерархии зависимостей по адресам

Виртуальная память


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

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

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

Память современных компьютеров представляет собой


Память современных компьютеров представляет собой сложную иерархию запоминающих устройств различных объемов, стоимости и времени доступа. Помимо регистров и оперативной памяти в микропроцессорах имеется, по крайней мере, одноуровневая, а чаще двухуровневая кэш-память; для ускорения преобразования виртуальных адресов в физические используются буферы трансляции адресов. Логически связанный набор модулей микропроцессора, отвечающих за организацию работы с памятью, называется подсистемой управления памятью. Подсистема управления памятью является ключевым компонентом микропроцессора, и, естественно, к корректности ее функционирования предъявляются очень высокие требования. Поскольку подсистема имеет сложную, многоуровневую организацию, число различных ситуаций, возможных при ее работе, огромно, что не позволяет проверить ее вручную. Эффективное тестирование подсистемы управления памятью возможно только при использовании методов автоматизации разработки тестов. В существующей практике тестирования часто полагаются на случай — генерация тестов (как правило, в форме программ на языке ассемблера тестируемого микропроцессора) осуществляется автоматически, но случайным образом. Инженер, отвечающий за тестирование, может задавать распределение вероятностей появления тех или иных инструкций в тестовых программах и указывать события, возникающие при их выполнении. Такой подход позволяет обнаруживать многие ошибки, но не является систематическим и, соответственно, не гарантирует полноты тестирования. В данной работе рассматривается подход к автоматизации генерации тестовых программ для подсистемы управления памятью. Подход является дополнением к “случайным тестам” и позволяет обнаруживать сложные, нетривиальные ошибки в моделях микропроцессоров. В основе предлагаемой методики лежат формальные спецификации устройств, входящих в подсистему управления памятью, спецификации инструкций, работающих с памятью, и описание тестового покрытия на уровне отдельных инструкций. Для генерации тестовых программ используются комбинаторные техники — тестовые воздействия на микропроцессор строятся как цепочки инструкций небольшой длины, составленные из различных сочетаний тестовых ситуаций. Оставшаяся часть статьи организована следующим образом. Во втором, следующем за введением, разделе кратко описывается устройство подсистемы управления памятью современных микропроцессоров. Третий раздел рассматривает используемый подход к генерации тестовых программ. В нем также обсуждается специфика тестирования подсистемы управления памятью. Четвертый раздел представляет собой сравнение предлагаемой методики с существующими подходами. В пятом разделе рассказывается об опыте практического использования методики. Наконец, шестой раздел содержит заключение.
Экономическое и социальное развитие общества требует для своей поддержки все более сложное программное обеспечение (ПО). Масштабность современных программных систем приводит к тому, что такие системы строятся из многих компонентов, разрабатываемых в разных организациях огромным количеством людей. Для построения работоспособных систем на основе такого подхода необходимо уметь решать задачи обеспечения совместимости между различными их компонентами и их надежности в целом. Лучшее решение, которое было найдено на этом пути - выработка и соблюдение стандартов на программные интерфейсы между отдельными компонентами. Идея, лежащая в основе использования интерфейсных стандартов, проста - настаивая на их соблюдении, мы обеспечиваем компонентам, созданным разными разработчиками, возможность взаимодействовать через стандартизованные интерфейсы. Таким образом, их совместимость обеспечивается без введения чересчур жестких ограничений на возможные реализации, что ограничило бы как творчество отдельных разработчиков, так и инновационный потенциал компаний-поставщиков ПО. Этот подход хорошо работает, если стандарт определяет функциональность, реализуемую фиксируемым им интерфейсом, достаточно точно и недвусмысленно. Однако, тексты многих современных стандартов, описывающих требования к программным интерфейсам, далеко не так точны. Это объясняется историей их появления. Обычно такие стандарты разрабатываются под давлением требований рынка и должны принимать во внимание противоречивые интересы многих поставщиков программного обеспечения, интерфейс которого предполагается стандартизовать. В таких условиях только базовая функциональность может быть определена непротиворечивым образом, а для сложных и специфических функций каждая группа разработчиков предлагает свое собственное решение. Поскольку многие поставщики ПО уже вложили деньги в имеющиеся у них решения, им очень тяжело отказаться от этих инвестиций в пользу варианта одного из конкурентов, который при этом остается в выигрыше. Чаще всего группа по выработке стандарта приходит к компромиссу, при котором основные поставщики должны внести примерно одинаковые по объему изменения в ПО каждого из них, а в тех случаях, где необходима серьезная переработка, стандарт осознанно делается двусмысленным, чтобы любое из уже имеющихся решений могло декларировать соответствие ему. Это позволяет поставщикам ПО потратить приемлемые для них деньги на обеспечение соответствия своих систем стандарту, но в то же время подрывает столь желаемую совместимость различных его реализаций.

Тестирование подсистемы управления памятью микропроцессора


Тестирование подсистемы управления памятью микропроцессора является нетривиальной задачей, которую практически невозможно решить без применения методов автоматизации. В статье был рассмотрен подход к автоматической генерации тестовых программ для MMU. В отличие от распространенных на практике методов, предлагаемый подход имеет высокий уровень автоматизации и является систематичным. Сгенерированные тестовые программы могут содержать встроенные проверки (тестовые оракулы), что делает их пригодными для тестирования не только моделей микропроцессоров, написанных на языках описания аппаратуры, но и готовых микросхем.
Внедрение и обеспечение соблюдения стандартов на программные интерфейсы связаны с большим количеством сложных проблем, как технического характера, так и экономических и социальных. Тем не менее, все эксперты сходятся на том, что эта деятельность необходима для стабильного развития производства программного обеспечения. Формализация стандартов уже неоднократно предлагалась в качестве возможного решения множества технических задач, связанных с ней. Однако формализация требует огромных усилий, что позволяет усомниться в применимости подходов на ее основе к реально используемым программным стандартам, достаточно объемным и сложным. Устранить эти сомнения помогут только практические примеры применения подобных методов к таким стандартам. Упомянутый выше проект [] по формализации LSB похоже, является одной из первых попыток формализации значительной части используемого на практике стандарта высокой сложности и позволяет почувствовать все ее выгоды и недостатки. Мы считаем, что представленный в этой статье подход позволит успешно справляться с подобными задачами. Аргументами в пользу этого мнения являются стабильное продвижение описанного проекта, история применений технологических составляющих данного подхода на практике ([,]) и лежащие в его основе базовые принципы хорошего проектирования больших систем [,]. Тот факт, что многие инженерные и организационные вопросы также находят отражение в данном подходе, позволяет надеяться на получение действительно полезных результатов, избежав участи многих проектов по использованию передовых методик разработки ПО на практике. Описанный проект является частью проводимых международным сообществом исследований подходов к решению проблемы обеспечения развития надежных крупномасштабных программных систем, одного из Больших Вызовов в информатике [,]. Тони Xoap (Tony Hoar) предложил вести работы по этой проблеме в двух направлениях: разрабатывать методы и инструменты, в перспективе способные помочь в ее решении, и накапливать примеры использования подобных методов на программных системах реалистичных размеров и сложности ("challenge codes").

Инженерный калькулятор


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

Из требований к приложению выделим поддержку 5-ти операционных систем с 4 основными языками локализации и выполнение, скажем, 50 инженерных функций, каждая из которых однозначно покрывается одним тестом. Тесты, безусловно, включают проверку на контрольных примерах, проверку на граничные значения и т.п. Кроме того, приложение позволяет выполнять, к примеру, 5 функций по взаимодействию с системой (запуск приложения, выход из приложения, сохранение результатов в файл, работа с буфером и т.п.).

Полное покрытие требований задаёт набор из 5*4*(50+5)=1100 тестовых прогонов. Так как нужно выполнить все доступные операции приложения под всеми поддерживаемыми операционными системами и языковыми локализациями.

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

Вернёмся к разбивке функциональности приложения по логическим группам. Есть набор инженерной функциональности и есть системное взаимодействие.

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

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

Взаимодействие с системой, запуск, остановку приложения, работу с буфером, запись результатов в файл, в нашем примере обеспечивает 5 тестов. Имеем 5 версий операционной системы и 4 локализации, что даёт 20 комбинаций и 20*5=100 тестовых прогонов.

Таким образом, для данного примера, полный охват функциональности определяется 50+100=150 тестовых прогонов, из которых 50 тестов инженерных функций выполняются под одной конфигурацией и 5 тестов системных функций под 20 тестовыми окружениями.



Объём задач


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

Клиент: 50 тестов на работу с данными (ввод форм, расчёт данных на основе данных хранимых в словарях, поиск данных, редактирование словарей и т.п.), 10 тестов на работу с печатными формами (формирование периодов выборок, выбор типов отчётов, печать или экспорт в предопределенный список форматов и т.д.). Пусть для тестирования работы с системой требуется, как и в предыдущем случае, ещё 5 тестов. По аналогии с предыдущим примером можем получить следующую картинку: имея 2 окружения получаем (5+10)*2=30 тестовых прогонов для проверки функциональности связанной с самой операционной системой (включая функционал печати и экспорта во время которого создаются новые файлы в рамках файловой системы). Будем считать, что 50 тестовых прогонов реализующих проверку логики работы с данными, можно выполнять под одним окружением. Итог — 80 тестовых прогонов для тестирования клиента системы.

Объединим в рамках рассматриваемого примера тестирование функциональности сервера приложений и базы данных. Пусть сервер приложения реализует 20 команд по обработке данных и пользовательских сессий (без учёта работы с системными пулами соединений, функций сжатия передаваемого по сети трафика и т.п.). Сервер баз данных реализует 10 системных операций по архивации данных, построению статистики использования отчётов и ещё несколько подобных операций. Общий смысл заключается в том, что мы имеем конечный набор тестируемых операций, и так как конфигурации определены заранее, можем говорить о конечном наборе тестов, которые необходимо выполнить, что бы проверить работоспособность серверного функционала системы. Итог — 30 тестов на серверной стороне. Заметим, что в данном примере мы не затрагиваем нагрузочную составляющую тестирования: речь идёт только о функциональном тестировании.



Определение


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

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

Стратегия отвечает на вопросы:

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

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

Как видим, Стратегия тестирования, как артефакт, органично вписывается в План Тестирования. Шаблоны планов тестирования, предлагаемые различными методологиями, зачастую прямо включают одним из разделов описание стратегии тестировании или же включают описание стратегии в пункты плана отвечающие за тестирование конкретных частей функционала. К примеру, шаблон Rational Unified Process определяет стратегию тестирования как самый большой раздел плана тестирования.

Что идеологически более правильно: выделять стратегию для всего проекта, или разрабатывать конкретные подходы для каждой области тестирования, зависит от конкретного проекта. В современных распределённых системах, с несколькими типами рабочих мест и системными агентами, которые также выступают пользователями системы, логичнее выделять стратегию для каждого набора тестируемого функционала: модуля или подсистемы. Для более простых приложений можно предлагать тестирование всего приложение в рамках одной Стратегии.

Заказчик, зачастую, хочет контролировать процесс тестирования и видеть понимание задачи тестирования исполнителями по проекту. Для него стратегия тестирования это менее детальный документ-видение (vision) того, как будет тестироваться система в процессе разработки.



Практика


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

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

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



Распределённая система


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

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

Пусть система имеет «толстого» клиента, сервер приложения и сервер базы данных, которые могут функционировать на разных физических платформах. Основная логика ввода/вывода, валидации значений и построения печатных форм сосредоточена на клиенте, сервер приложений обеспечивает презентационный уровень и необходимую сервисную логику (автоматическая архивация данных, оповещение пользователей о внештатных ситуациях и т.д.), а база данных обеспечивает кроме непосредственного хранения данных определённую часть обработки данных реализуемую, к примеру, в виде пакетов функций. Ничего особенно сложного выбрано не было и пусть описание не пугает определённой утрированностью — мы всего лишь обрисовали задачу для тестирования.

Для того, чтобы чрезмерно не усложнять задачу, сузим область тестовых окружений за счёт фиксированных конфигураций для сервера БД (зачастую в промышленной эксплуатации используется выделенный сервер или кластерное решение для работы баз данных) и сервера приложений. Клиентское приложение обеспечивает работу под 2-мя операционными системами и жестко фиксированной конфигурацией компонентов ОС (к примеру, на все пользовательские машины регулярно устанавливаются все обновления и service packs + набор ПО на клиентских машинах строго регламентирован внутренней IT политикой).



Стратегия тестирования в действии


В данном случае, мы разобрались с тем, что именно нужно тестировать, определили требования к тестам, провели простейший анализ условий тестирования и зависимостей. По сути своей, определили стратегию: «как» и «что» тестировать. Разрабатывать тесты можно в параллель к разработке стратегии, в любом случае дизайн и разработку тестов стратегия не заменяет.

Полное покрытие требований определяет 1100 тестовых прогонов. Разбор функциональности и определение необходимого набора тестирования даёт на выходе из разработки стратегии тестирования 150 тестовых прогонов.

Для данного конкретного примера, разработка стратегии тестирования даёт прямой выигрыш в затратах на тестирование в 1100/150=7,(3) раза. Как видим, имеет смысл. Попробуем рассмотреть шаги разработки стратегии тестирования для более сложного приложения — распределённой клиент-сервер системы.



Терминология


СТРАТЕГИЯ - искусство руководства; общий план ведения этой работы, исходя из сложившейся действительности на данном этапе развития.

Есть другой вариант, с военным уклоном:

Наука о ведении войны, искусство ведения войны. Общий план ведения войны, боевых операций.

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



Вступление


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

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



в общем случае даёт разработка


Что в общем случае даёт разработка стратегии тестирования? Разбор задачи тестирования на составляющие, выделение тестовых областей и в конечном итоге более полное понимание задачи тестирования в конкретном проекте. Как мы видели на примере тестирования инженерного калькулятора, понимание задачи позволяет разделять функциональность тестируемого приложения или системы на области, которые могут тестироваться автономно, что позволяет снизить (и порой достаточно существенно!) затраты на тестирование.
Вернёмся к определению понятия Стратегия: «общий план ведения работы, исходя из сложившейся действительности на данном этапе развития» — применимо к тестированию, стратегия есть понимание «что», «где» (на каком окружении) и «когда» будет тестироваться. Ответ на вопрос «как» нам может дать анализ требований к системе и дизайн тестов. Согласитесь, имея перед собой план развития системы и функционала, можно достаточно уверенно планировать задачи по тестированию, готовить необходимые данные и тестовые окружения. В любой момент времени, руководитель, полагаюсь на стратегию, знает где он находится и куда двигается дальше. Планируя тестирование, руководитель отдела или ведущий тестировщик не начинает разбираться с системой, а занимается непосредственно планированием, выделением ресурсов и сроков на конкретные задачи.

Зависимости от артефактов проекта


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

Что следует учитывать разрабатывая стратегию тестирования для сложных распределённых или клиент-серверных систем. Одним из основных факторов, влияющих на стратегию тестирования (после выделения тестовых областей и понимания тестовых задач в каждой из областей), является анализ план-графика появления новой функциональности в системе, а зачастую и плана разработки дизайнов и спецификаций к модулям и компонентам системы (от этого зависит выполнение задачи дизайна тестов). Чёткий план проекта, разбитый по задачам планирования, проектирования, дизайна и имплементации даёт менеджеру тестирования фундамент, необходимый для решения задачи, которую ставит вопрос «когда», то есть вопрос связанный с планированием тестирования. Хотя временные оценки и оценка трудоёмкости задачи логически выходят за рамки определения стратегии и относятся к непосредственному планированию тестирования, именно понимание приложения/системы в разрезе его развития с течением проекта, является стратегической составляющей плана тестирования.

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



Анализ документации и разбиение тестируемых интерфейсов на группы


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

Иногда разбиение интерфейсов на группы уже проведено в документации. Например, документация по библиотеке Glib [10] состоит из таких разделов, как "Arrays", "Unicode Manipulation", "Memory Allocation" и т.д. Интерфейсы, описанные в каждом таком разделе, как правило, принадлежат к одной функциональной группе.


Рисунок 3. Разработка тестов с помощью системы T2C.

При разработке тестов для группы интерфейсов создаётся один или более T2C-файлов. На этом этапе удобно создать и необходимую структуру каталогов для тестового набора.

Ниже предполагается, что документация по тестируемым интерфейсам является набором html-документов.



Автоматизация разработки TET-совместимых тестов в GTK+-Verification Test Suite (GTKVTS)


Подход, использующийся в GTK+-2.0 Verification Test Suite (GTKVTS) при разработке TET-совместимых тестов, позволяет преодолеть некоторые из описанных выше недостатков TET [8].

Во-первых, в GTKVTS используются т.н. параметризуемые тесты. То есть разработчик пишет шаблон кода тестов на обычном С, отмечая в нём особым образом места, куда впоследствии будут вставлены значения параметров теста. Для каждого такого шаблона может быть задано несколько наборов параметров. Параметром может быть практически что угодно, не только параметр тестируемой функции или её ожидаемое возвращаемое значение. Иногда удобно вынести в качестве параметров типы используемых данных (наподобие template в С++) или даже сам вызов тестируемой функции и т.д.

По шаблону кода теста для каждого набора параметров генератор С-кода GTKVTS создаёт отдельную функцию на языке С (см. Рис. 1).


Рисунок 1. Генерация С-кода тестов по шаблону. "<%0%>" и "<%1%>" в шаблоне кода теста — места для вставки параметров.

Во-вторых, при генерации кода тестов на С средства GTKVTS автоматически вставляют в этот код определения данных, необходимых для работы теста в среде TET, так что разработчику не приходится заботиться об этом самому. Помимо этого, автоматически создаются и make-файлы, необходимые для сборки тестов, а также файлы сценариев TET, что тоже удобно.

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

К менее существенным недостаткам средств GTKVTS стоит также отнести отсутствие поддержки отладки теста вне TET, а также специализированность инструментальных средств для использования при разработке тестов только для библиотек из стека GTK+.



Check


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

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

Тест представляет собой код на языке программирования Си, заключённый между макросами START_TEST и END_TEST. Проверки требований в тестах выполняются с использованием функций fail_unless(проверяющее_выражение, "текст, описывающий ошибку") и fail_if(проверяющее_выражение, "текст, описывающий ошибку").

Как для каждого теста, так и для набора тестов можно задать функции, выполняющие инициализацию и освобождение используемых ресурсов (т.н. checked и unchecked fixtures)

К достоинствам системы Check стоит отнести:

возможность выполнения каждого теста в отдельном процессе, т.е. изоляция тестов друг от друга и от самой среды Check;

автоматическая обработка исключительных ситуаций в тестах;

возможность задания максимально допустимого времени выполнения теста;

специальные средства для проверки ситуаций, когда выполнение тестируемой функции приводит к посылке сигнала;

интеграция системы сборки и запуска тестов с autoconf и automake — широко используемыми средствами автоматизации процесса сборки и установки ПО [4].

Система Check имеет, тем не менее, некоторые недостатки, из-за которых использование её не всегда оправдано.

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

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

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

Не поддерживается вывод стандартных кодов результата теста, определённых в стандарте на тестирование соответствия POSIX [5].



CUnit


Система CUnit [6] может использоваться в тех же случаях, что и Check [3], но обладает в целом более скромными возможностями.

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

Также в отличие от Check в CUnit нет защиты от "зависания" тестов (нельзя задать максимально допустимое время работы теста).

CUnit имеет и некоторые преимущества по сравнению с Check.

Поддержка т.н. fatal и non-fatal assertions. В первом случае, если проверка выявила нарушение требования, выполнение теста прекращается и остальные проверки в нём, таким образом, не выполняются (в Check всё происходит именно так). Если же нарушение требования выявлено в non-fatal assertion, выполнение теста продолжается. Возможно, остальные проверки в данном тесте позволят в таком случае дать разработчику более полную информацию о том, что происходит в тестируемой системе. Это может быть полезно при выяснении причины обнаруженного сбоя.

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

Поддержка вывода отчётов о выполнении тестов в разных форматах, в том числе пригодных для отображения в web-браузере (xml+xslt).

Тем не менее, указанные в предыдущем разделе недостатки системы Check в полной мере относятся и к CUnit. Test Environment Toolkit (TET), о котором речь пойдёт ниже, свободен от некоторых из этих недостатков.



Достоинства и недостатки существующих решений


Мы рассмотрели 5 подходов к разработке тестов для программных интерфейсов на языке программирования Си. Достоинства и недостатки каждого из них сведены в Таблице 1.

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

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

MANUAL

Check

CUnit

TET

GTKVTS

Параметризация тестов

+

Прослеживаемость требований стандарта

Запуск тестов в отдельном процессе

+

Автоматическая обработка исключительных ситуаций

+

+

+

+

Ограничение времени работы теста

+

+

+

Иерархическая организация пакетов

+

Удобство отладки тестов

+

+

+

Переносимость тестов

+

+

+

Использование стандартных видов вердиктов [5]

+

+

Таблица 1. Сравнение существующих подходов.



Генерация кода тестов, make-файлов и сценариев TET


Когда подготовлены тесты в T2C-формате и создан каталог требований, следует запустить генератор кода (T2C Code Generator), который создаст С-файлы с кодом тестов, make-файлы для их сборки, сценарии TET и т.д.



Литература


Домашняя страница CTESK. The Linux Standard Base. Домашняя страница Check. Домашняя страница Autoconf и Automake. IEEE.2003.1-1992 IEEE Standard for Information Technology — Test Methods for Measuring Conformance to POSIX — Part 1: System Interfaces. IEEE, New York, NY, USA, 1992. ISBN 1-55937-275-3. Домашняя страница CUnit. TETware User Guide. GTKVTS Readme. В.В.Кулямин, Н.В.Пакулин, О.Л.Петренко, А.А.Сортов, А.В.Хорошилов. Формализация требований на практике. Препринт ИСП РАН, Москва, 2006. Glib Reference Manual. Центр верификации ОС Linux. Домашний сайт UniTESK. Домашняя страница проекта LSB Infrastructure.


MANUAL


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

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

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

В проекте по тестированию POSIX-совместимой операционной системы реального времени для встраиваемых устройств, в отделе технологий программирования ИСП РАН был разработан подход MANUAL, предназначенный для разработки тестов, проверяющих базовую функциональность операционной системы. Эти тесты проверяли работоспособность ключевых компонентов операционной системы перед началом тщательного тестирования тестовой системой, разработанной при поддержке инструментов CTESK.

Тесты MANUAL представляют собой код на языке программирования Си, использующий макросы и функции библиотеки поддержки MANUAL. Каждое тестовое испытание записывается в виде отдельной функции, начинающейся с макроса TC_START(“имя тестового испытания”) и заканчивающейся макросом TC_END(). Тело теста состоит из трех частей:

подготовке окружения и данных;

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

освобождения ресурсов.

Проверка корректности работы тестируемой системы проводится при помощи функции tc_assert(проверяющее_выражение, “текст, описывающий ошибку”).
Если проверяющее выражение оказывается равным false, то система считает, что произошла ошибка в тестируемой системе и выводит сообщение, описывающее ее. Кроме того, система автоматически отлавливает в ходе выполнения теста исключительные ситуации, что приравнивается к обнаружению ошибки.

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

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

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


Общие сведения.


Система T2C ("Template-to-Code"), позволяет автоматизировать разработку параметризованных тестов, работающих как в среде TET, так и вне её.

Код тестов на языке С создается на основе T2C-файла, в котором расположены шаблоны тестов и наборы параметров для них (схема та же, что и для GTKVTS — см. Рис. 1). Фрагмент T2C-файла показан ниже.


Рисунок 2. Фрагмент t2c-файла.

Тесты, которые будут созданы по шаблону, показанному на Рис. 2, имеют два параметра: TYPE и INDEX. В первом из сгенерированных тестов в качестве TYPE будет использоваться int, в качестве INDEX — 6, во втором — double и 999, соответственно.

Также, как и в GTKVTS, в код теста на С автоматически добавляются все необходимые определения данных и функций для выполнения этого теста в среде TET, а также генерируются make-файл и файл сценария TET.

Таким образом, инструменты T2C сохраняют основные достоинства системы GTKVTS, но при этом они поддерживают рекомендации по разработке тестов соответствия, сформулированные, в частности, в [9]:

составление каталога элементарных требований к тестируемым программным интерфейсам;

привязка проверок, выполняемых в тестах, к соответствующим местам в тексте стандарта;

оценка качества тестирования в терминах покрытия элементарных требований.

В T2C также сделаны следующие усовершенствования по сравнению с GTKVTS.

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

Есть возможность создания независимой версии теста на чистом C/C++, не использующей средства TET. Это может быть полезно для отладки самого теста или для более тщательного исследования поведения тестируемой системы в случае отладки ошибки в реализации.

Заготовки для T2C-файлов создаются автоматически по размеченному тексту стандарта.

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

Поддерживается выполнение тестов в отдельных процессах.

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



Подготовка каталога требований


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



Применение T при разработке тестов для LSB Desktop


Система T2C использовалась (и используется в настоящее время) при разработке тестов для интерфейсных операций ("интерфейсов") библиотек Linux, входящих в стандарт Linux Standard Base (LSB). C её помощью подготовлены тесты базовой функциональности для следующих библиотек:

Glib (libglib-2.0);

GModule (libgmodule-2.0);

ATK (libatk-1.0);

Fontconfig (libfontconfig).

В Таблице 2 представлены результаты тестирования. Описания найденных ошибок опубликованы на http://linuxtesting.ru/results/impl_reports.

Библиотека

Версия

Всего интерфейсов

Протестировано

Найдено ошибок в реализации

libatk-1.0

1.19.6

222

222 (100%)

11

libglib-2.0

2.14.0

847

832 (98%)

13

libgmodule-2.0

2.14.0

8

8 (100%)

2

libfontconfig

2.4.2

160

133 (83%)

11

Всего

1237

1195 (97%)

37

Таблица 2. Результаты тестирования LSB библиотек тестами T2C.

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

Примечание 2. В столбце "Всего интерфейсов" указано общее количество входящих в LSB интерфейсных операций ("интерфейсов") библиотеки, включая и недокументированные. Практически все документированные интерфейсы были протестированы.

Средние затраты на полный цикл разработки теста (от разметки и анализа требований до отлаженного кода теста) для одного интерфейса составили порядка 0,5-1 человеко-дня.

Отметим, что интерфейсы в документации по данным библиотекам далеко не всегда описаны подробно. В среднем для каждого интерфейса было выделено 2-3 элементарных требования.

Сведения о количестве выделенных элементарных требований к тестируемым интерфейсам и о покрытии этих требований представлены в Таблице 3.


Библиотека

Выделено требований

Проверено требований

Покрытие по требованиям, %

libatk-1.0

515

497

96%

libglib-2.0

2461

2290

93%

libgmodule-2.0

21

17

80%

libfontconfig

236

177

75%

Всего

3233

2981

92%

Таблица 3. Выделенные и проверяемые требования для LSB библиотек.


Процесс разработки тестов с помощью системы T


В этом разделе рассматриваются основные стадии процесса разработки тестов с помощью T2C.



Разметка требований в документации


На данном этапе в документации необходимо выделить элементарные требования к тестируемым интерфейсам и присвоить каждому требованию уникальный идентификатор [9]. При необходимости текст требований можно переформулировать для улучшения читаемости.

Разметка требований выполняется в html-редакторе KompoZer (www.kompozer.net) с использованием инструмента ReqMarkup, разработанного в рамках проекта OLVER [11] и затем интегрированного в систему T2C.



Разработка тестов в T-формате


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

При редактировании T2C-файла может оказаться полезным компонент T2C Editor — plug-in для Eclipse IDE. Он обеспечивает удобную навигацию по секциям T2C файла, предоставляет средства для работы с параметрами тестов и т.д.



Сборка, запуск и отладка тестов


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



Создание шаблона T-файла


Когда выделены и размечены все требования для интерфейсов из данной функциональной группы, с помощью ReqMarkup автоматически создаётся заготовка T2C-файла.



T: технология автоматизированной


, ,
Труды Института системного программирования РАН

Аннотация. В статье обсуждается задача автоматизации разработки тестов базовой функциональности программных интерфейсов (API). Рассматриваются существующие решения по разработке таких тестов и описываются разработанные в ИСП РАН технология и инструментальные средства T2C, нацеленные на создание тестов «среднего» уровня по качеству тестирования и объему соответствующих трудозатрат. Приводится статистика результатов использования T2C в рамках проекта по тестированию интерфейсов системных библиотек Linux, описываемых стандартом LSB.



TET (Test Environment Toolkit)


Система TETware (TET, Test Environment Toolkit) достаточно широко используется для тестирования различных программных интерфейсов. Средства TET позволяют запускать разные тесты единым образом и получать отчёт о работе тестов в едином формате [7]. Информация о выполнении теста, включая его результат и сообщения, которые в нём выводятся, попадает в так называемый журнал TET.

Основными компонентами TET являются:

test case controller (tcc) — этот компонент управляет запуском тестов и сбором выводимой ими информации;

интерфейс прикладного программирования (TET API), который нужно использовать в тестах для того, чтобы их можно было выполнять в среде TET. TET API существует для различных языков программирования, в том числе и для С/С++.

Наиболее существенными достоинствами TET, на наш взгляд, являются следующие:

единая среда для запуска тестов;

обработка исключительных ситуаций в тестах (например, segmentation fault) средствами test case controller;

общие для всех тестов допустимые коды результата, соответствующие стандарту [5]: PASS, FAIL, UNRESOLVED и т.д., плюс возможность определить дополнительные коды результата тестов;

возможность добавлять новые тесты в набор без перекомпиляции существующих тестов (использование т.н. сценариев TET)

единый формат отчёта о выполнении тестов (журнала TET).

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

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

Отсутствие средств для связи проверок, проводимых в тестах, с текстом соответствующего стандарта.


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

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

Тесты, запускаемые с помощью test case controller, не всегда удобно отлаживать. Как для поиска ошибок в тесте, так и для исследования поведения тестируемой системы в случае сбоя полезно исключить влияние средств TET на работу теста, что существенно упрощает использование программ-отладчиков (например, gdb).

Ниже речь пойдёт как раз о "надстройках" над TET (системы GTKVTS и T2C), в которых перечисленные выше недостатки в той или иной степени преодолены.


Хорошо известно, что проверить корректность


Хорошо известно, что проверить корректность работы любой серьезной программной системы во всех ситуациях, которые могут встретиться в ходе ее реальной эксплуатации, невозможно. Поэтому тестирование программного обеспечения неизбежно связано с поиском компромисса между тщательностью проверок и ограничениями на имеющиеся ресурсы.
Поиск оптимального решения зависит от множества факторов, определяющих специфику конкретного проекта. В данной работе мы рассмотрим разработку тестов, предназначенных для тестирования на соответствие требованиям стандарта программных интерфейсов на языке программирования Си. Такая постановка задачи предопределяет значение целого ряда факторов.
Необходимость сопровождения тестов вместе с развитием стандарта.
Наличие стандарта подразумевает существование достаточно качественного описания требований к функциональности тестируемой системы. Хотя для стандартов, находящихся в стадии активного развития, такое предположение не всегда оказывается верным.
Потребность в простоте анализа обнаруживаемых ошибок, так как во многих случаях ошибки в реализации будут анализировать не разработчики тестов, а представители сторонних организаций, проверяющих свои продукты на соответствие требованиям стандарта.
Эти особенности формируют ключевые требования к результирующему тестовому набору, и тем самым, к подходу по его разработке. Но из них не следует никаких ограничений на возможные решения конфликта между детальностью тестирования и имеющимися ресурсами.
Наиболее распространенным решением при разработке тестов на соответствие является выбор тестирования базовой функциональности, под которым понимается тестирование, обеспечивающее проверку корректности функционирования системы на нескольких основных вариантах её использования, включающих в себя и некоторые ошибочные ситуации. Распространенность такого выбора объясняется тем, что тестирование базовой функциональности позволяет с минимальными затратами выявить большинство наиболее грубых отклонений от стандарта.


Существуют и альтернативные решения. В случаях, когда в короткие сроки требуется обеспечить покрытие очень большого количества функций, выбор падает на менее тщательное тестирование, например, такое, которое нацелено на проверку только минимальной работоспособности каждой функции. Одна из технологий разработки тестов такого вида — технология Azov — представлена в настоящем сборнике.
В ситуациях, когда обеспечение точного соответствия стандарту является крайне важным и подкрепляется наличием достаточного количества ресурсов, выбирается более тщательное тестирование, например, такое, которое нацелено на проверку всех выделенных классов тестовых ситуаций для каждого отдельного аспекта функциональности каждой интерфейсной функции. Примером инструментов, поддерживающих разработку столь детальных тестов, которые при этом еще и остаются удобными для дальнейшего сопровождения и развития, является набор инструментов CTESK [1]. Такие особенности CTESK как:
наличие средств для формального описания требований к тестируемой системе;
поддержка автоматической генерации последовательностей тестовых воздействий при помощи динамического построения модели поведения тестируемой системы;
широкий набор возможностей для задания метрик качества тестирования в терминах модели требований с автоматизацией сбора информации о достигнутом покрытии;
помогают организовать систематический перебор всех тестовых ситуаций и упростить анализ тестового покрытия.
Набор инструментов T2C, о котором пойдет речь в настоящей статье, нацелен на разработку тестов базовой функциональности. Превосходя возможности технологии Azov и уступая инструментам CTESK по уровню тщательности тестирования получаемых тестов, инструменты T2C позволяют эффективно достигать середины в балансе между качеством тестирования и ресурсами, необходимыми для построения соответствующих тестов. При этом инструменты T2C поддерживают основные рекомендации по работе с требованиями при разработке тестов на соответствие, такие как составление каталога элементарных требований стандарта, обеспечение прослеживаемости требований стандарта в тестах и измерение качества тестирования в терминах покрытия элементарных требований.
Данная статья построена следующим образом. В первом разделе мы рассмотрим ряд подходов, решающих близкие задачи, а также обсудим их достоинства и недостатки применительно к стоящим перед нами целям. Далее мы представим основные особенности инструментов T2C и поддерживаемого ими процесса разработки. Результаты будут проиллюстрированы на опыте применения данного подхода при разработке тестов на соответствие требованиям стандарта LSB [2] для ряда библиотек из стека GTK+ и fontconfig. В заключении мы обсудим возможные направления дальнейшего развития подхода и его интеграции с другими инструментами семейства UniTESK.

Задача тестирования программных интерфейсов на


Задача тестирования программных интерфейсов на соответствие документации (в том числе стандартам) является важным звеном в обеспечении качества и интероперабельности программных систем. Для решения этой задачи разрабатываются различные методики и инструменты, позволяющие систематизировать и автоматизировать работу. При этом всегда возникает выбор баланса между качеством тестирования и затратами на разработку соответствующих тестов. Решение здесь обычно принимается директивно из достаточно субъективных соображений. Тем временем, с выбором целевого качества тестирования связан и выбор оптимальной технологии и инструментов, так как для различных уровней цены и качества необходимы разные подходы. Так, например, для глубокого тестирования важных интерфейсов хорошо зарекомендовала себя технология UniTESK [12], которая, однако, требует и соответствующих высоких затрат как на освоение технологии, так и на разработку самих тестов.
В данной работе рассмотрена технология T2C, нацеленная на эффективную разработку тестов «среднего уровня» для проверки базовой функциональности программных интерфейсов. В данном случае понятие «средний уровень» соответствует наиболее распространенным представлениям о качестве производственного тестирования, наблюдаемом в большинстве изученных авторами тестовых наборов (например, сертификационные тесты Open Group, сертификационные тесты LSB, тесты OpenPosix и Linux Test Project). T2C позволяет повысить эффективность разработки таких тестов за счет использования следующих основных возможностей, минимизирующих ручной труд на создание окружения и дублирование кода, не принципиального для специфической логики конкретного теста:
Автоматическая генерация заготовок тестов на основе каталога требований.
Использование именованных параметров в коде тестов, для каждого набора значений которых создается соответствующий экземпляр теста.
Высокоуровневое API для использования в коде тестов для осуществления проверок и трассировки.


Генерация независимых отдельных тестов в виде самодостаточных простых программ на C/C++, что в практическом плане существенно упрощает отладку тестов и реализации (в отличие от отладки в полной среде запуска TET или подобной).
Среда выполнения тестов, созданных по технологии T2C, опирается на широко распространенные средства TETware, что позволяет легко интегрировать тесты в существующие тестовые наборы и окружения управления тестированием и анализа результатов. Кроме того, важной особенностью технологии T2C является систематизированная работа с каталогом требований к проверяемым интерфейсам и обеспечение связи отдельных требований с проверками в тестах и соответствующими сообщениями в отчете о результатах тестирования.
Технология T2C была успешно применена в Институте системного программирования РАН в проекте [13] по разработке сертификационных тестов для проверки соответствия библиотек Linux стандарту LSB. В работе представлена статистика по разработанным тестам, найденным ошибкам и трудозатратам, которая позволяет сделать вывод об эффективности данной технологии для заданного сегмента качества разработки тестов программных интерфейсов различных модулей и библиотек. В настоящее время реализована инструментальная поддержка Си и Си++, но нет принципиальных проблем и для других современных языков программирования общего назначения, например C# или Java. Стоит отметить, что важным фактором для успешного применения T2C является наличие достаточно устоявшегося текста требований, так как этап анализа и составления каталога требований в условиях частых изменений и плохого качества описаний может составлять существенную часть трудозатрат.
В дальнейшем в T2C предполагается реализовать средства для автоматизации работы с множествами значений параметров тестов. Будет повышена и степень интеграции инструментов T2C со средой разработки Eclipse. Также предполагается исследовать возможности интеграции систем T2C и CTESK.