Аннотация. В
данной статье рассматривается задача адаптации технологии Azov для
построения тестового набора, проверяющего работоспособность
интерфейса библиотеки Qt3 для разработки приложений с графическим
пользовательским интерфейсом. Приводится уточнение базовой методики,
которое позволяет формировать корректные тесты с учетом специфики
языка C++ и дополнительных возможностей тестируемой библиотеки.
Вводятся расширения технологии, позволяющие ускорить работу над
созданием тестового набора. Полученная методика показывает высокую
эффективность при разработке простейших тестов работоспособности для
сложных интерфейсов, содержащих большое количество методов и функций.
Отдельно обсуждаются достоинства и недостатки технологии, выявленные
в процессе ее реализации, а также указываются возможные направления
ее дальнейшего развития.
Содержание
- 1. Введение
- 2. Методика построения корректных тестов
- 2.1. Конструкторы и деструкторы
- 2.2. Абстрактные классы
- 2.3. Защищенные методы
- 2.4. Методы, не входящие в программный интерфейс приложения
- 2.5. Среда исполнения тестируемого метода
- 2.6. Сигналы и слоты
- 2.7. Завершение теста и отложенное выполнение целевого воздействия
- 3. Дополнительное обеспечение корректности тестового набора
- 3.1. Проверки, добавляемые автоматически
- 3.2. Контроль корректности наложенных ограничений
- 4. Оптимизация разработки тестов
- 4.1. Приведение типов и наследование
- 4.2. Удобство и функциональность интерфейса
- 4.3. Разбиение на группы и порядок выполнения работ
- 5. Достоинства и недостатки подхода
- 6. Заключение
- Литература
1. Введение
Тестирование работы программы в различных ситуациях
остается одним из самых широко используемых способов демонстрации ее
корректности, особенно для достаточно сложного приложения. Но все
существующие методики создания тестов, обеспечивающие их полноту в
соответствии с некоторыми естественными критериями, требуют весьма
значительных трудозатрат.
Однако в ряде случаев, например, при тестировании же
объемных интерфейсов, содержащих тысячи операций, полное и тщательное
тестирование обходится слишком дорого, а иногда и вовсе не требуется
для всех элементов интерфейса системы. Вместо этого проводится
тестирование работоспособности, то есть проверяется, что все функции
системы устойчиво работают хотя бы на простейших сценариях
использования. Уже затем, для наиболее критической части интерфейса
разрабатываются детальные тесты.
Именно для решения таких типов задач предназначена
технология Azov [1], созданная в 2007 году в Институте
системного программирования. Технология нацелена на разработку тестов
работоспособности, вызывающих тестируемые операции в правильном
окружении с какими-нибудь допустимыми значениями параметров,
характеризующими простейшие сценарии использования этих операций. При
этом она позволяет существенно автоматизировать создание теста.
Область применимости технологии включает те случаи,
когда информация об интерфейсе доступна в хорошо структурированном,
годном для автоматической обработки виде. При этом трудозатраты на
создание одного теста существенно меньше, если тестируемый интерфейс
содержит большое количество операций.
В частности технология Azov была использована при
создании тестового набора для бинарных операций библиотеки Qt3,
предназначенной для разработки приложений с графическим
интерфейсом [4] и входящей в стандарт Linux Standard
Base [2,3] (LSB). Стандарт включает в себя около
10000 методов и функций библиотеки и
хранит информацию об их синтаксисе в своей базе данных.
Тестирование библиотек, написанных на языке C++, в
частности Qt3 [4], является достаточно трудоемкой задачей.
Поведение метода может зависеть не только от передаваемых ему
параметров и глобального состояния системы, но и от состояния
объекта, которому он принадлежит. Сама же библиотека графического
интерфейса содержит дополнительные механизмы, которые также оказывают
существенное влияние на работу методов.
Поэтому базовая методика, предлагаемая технологией
Azov, должна быть дополнена набором средств, позволяющих формировать
работоспособные тесты на языке C++ во всевозможных ситуациях,
возникающих при тестировании элементов интерфейса, и учитывающих
особенности библиотеки Qt [3], в
частности механизм передачи сообщений.
2. Методика построения корректных тестов
Основной задачей рассматриваемой технологии является
построение корректного теста работоспособности для каждой операции,
входящей в тестируемый интерфейс.
Под корректным понимается такой тест, который
обеспечивает правильную последовательность инициализации параметров
целевой операции, инициализацию параметров среды исполнения, содержит
непосредственный вызов целевой операции, а также освобождает по
завершении задействованные ресурсы и возвращает систему в исходное
состояние. Следует также отметить, что в тесте должно проверятся,
выполнился ли вызов тестируемой операции успешно или наблюдается
отклонение от ожидаемого поведения.
Вопросы, касающиеся инициализации и финализации
(освобождение захваченных тестом ресурсов) тестовых данных относятся
скорее к технологии в целом, и освещены в статье [1]. Здесь же
речь пойдет об особенностях реализации общего подхода технологии Azov
при использовании языка C++ и для тестирования библиотеки Qt3.
Описанные ниже особенности обеспечиваются
функциональностью инструмента-компоновщика тестов, генерирующего
тесты по базе данных с уточненной информацией об интерфейсных
операциях [1,5], и разработчику, как правило, не приходится о
них беспокоиться.
2.1. Конструкторы и деструкторы
В языке C++ каждому конструктору на программном
уровне соответствуют два различных конструктора на бинарном уровне:
in-charge и not-in-charge, а каждому деструктору –
два или три бинарных: in-charge, not-in-charge и,
дополнительно для виртуальных классов, – in-charge deleting.
Поскольку стандарт LSB описывает поведение системы на бинарном
уровне, то тестированию подлежат все вышеперечисленные конструкторы и
деструкторы.
In-charge конструкторы и деструкторы
вызываются при непосредственной работе с объектом класса. In-charge
deleting деструктор используется при удалении объекта
виртуального класса из общей памяти. Таким образом, для конструктора
и деструктора невиртуального класса компоновщик составляет тест,
создающий объект либо в стековой, либо в общей памяти, а затем
удаляющий его. В случае виртуального класса область памяти, в которой
будет создаваться объект, зависит от тестируемого деструктора.
Not-in-charge конструкторы и деструкторы
используются неявно при работе с объектом какого-либо класса,
наследующего данному. Поэтому в тесте описывается наследник, и путем
создания и уничтожения объекта полученного типа опосредованно
выполняется вызов этих конструкторов и деструкторов.
2.2. Абстрактные классы
При тестировании ряда методов возникает
необходимость построения объекта абстрактного класса. Следует,
однако, уточнить, что непосредственно такой объект необходим только в
том случае, когда тестируется его метод. В остальных же случаях
можно, как правило, обойтись каким-либо объектом наследующего класса.
В первом случае необходимо провести дополнительную
работу по определению чисто виртуальных методов. Реализация
функциональности каждого метода является трудоемкой задачей, решения
которой при проведении тестирования работоспособности хотелось бы
избежать. Поэтому компоновщик тестов автоматически генерирует код, в
котором определяется класс-наследник абстрактного класса, имеющий те
же методы, что и родитель. Чисто виртуальные методы реализованы как
заглушки, возвращающие какое-либо значение необходимого типа данных.
Это значение получается по общим правилам генерации тестов, то есть
может быть либо взято из предопределенного пользователем множества,
либо сконструировано компоновщиком.
Впрочем, в ряде случаев такой подход оказывается
неприемлемым. Например, если заглушка используется каким-либо другим
методом, вызываемым в процессе тестирования. В подобных ситуациях,
если это явилось причиной падения теста, необходимо вручную
реализовывать чисто виртуальные методы, основываясь, как правило, на
исходных кодах одного из существующих наследников тестируемого
класса.
Дополнительно, стоит отметить, что язык C++
допускает реализацию по умолчанию абстрактного метода, которая может
быть использована при описании наследника. Однако в библиотеке Qt3
такая возможность не используется.
В тех же случаях, когда объект абстрактного класса
выступает в качестве параметра, возможны различные варианты его
инициализации. Во-первых, можно воспользоваться описанным выше
способом и позволить компоновщику сконструировать объект. Во-вторых,
можно доопределить абстрактный класс вручную. Однако более
предпочтительным является использование объекта какого-либо класса,
наследующего данному. Также могут встретиться методы, возвращающие
указатель на нужный абстрактный класс. Разумеется, в действительности
они возвращают указатель на объект наследующего класса. Результат
работы таких методов также можно использовать в качестве значения
параметра.
Аналогично, при тестировании невиртуальных методов
можно не доопределять абстрактный класс, а воспользоваться
экземпляром одного из наследников тестируемого класса.
In-charge
конструкторы и деструкторы абстрактного класса протестировать
невозможно, поскольку они предназначены для построения или разрушения
объекта именно этого класса.
2.3. Защищенные методы
Использование защищенных методов также требует
некоторых дополнительных построений. Область видимости таких методов
ограничена методами класса, которому принадлежит защищенный метод,
или его наследников.
При построении теста компоновщик генерирует описание
класса, наследующего классу, содержащему защищенный метод. В нем
определяется дополнительный общедоступный метод, являющийся оберткой
защищенного. Параметры обоих методов совпадают, так что везде в тесте
можно обращаться к построенному методу сгенерированного класса,
подразумевая вызов требуемого защищенного метода.
Из этого правила имеется исключение, а именно
случай, когда метод имеет в качестве одного из своих параметров
объект защищенного типа данных. В рамках Qt, например, встречаются
защищенные перечисления (protected enum). В такой ситуации цепочка
инициализации объекта нужного типа генерируется компоновщиком в теле
метода-обертки, а сам этот общедоступный метод имеет на один параметр
меньше.
Следует также добавить, что при таком подходе
защищенные in-charge конструкторы и деструкторы не могут быть
протестированы, поскольку они предназначены для работы с объектом
исходного класса, а вызвать их можно только из объекта наследующего
класса.
2.4. Методы, не входящие в программный интерфейс приложения
Определяя набор интерфейсных операций на бинарном
уровне, стандарт LSB в некоторых случаях включает в себя операции, не
являющиеся частью программного интерфейса (Application Programming
Interface, API) описываемых им библиотек. Однако, при тестировании
работоспособности необходимо, по возможности, протестировать даже
подобные скрытые операции.
В рамках Qt3 существует ряд классов, предназначенных
исключительно для использования внутри самой этой библиотеки.
Описания таких классов находятся либо в заголовочных файлах,
предназначенных только для сборки библиотеки, либо и вовсе в исходных
кодах.
Одним из возможных подходов к тестированию методов
таких классов является построение теста для метода, входящего в API,
в процессе работы которого вызывается скрытый метод. Однако данный
способ имеет большое количество недостатков. Во-первых, требуется
весьма трудоемкое исследование того, какие методы могут быть
использованы и какие значения их параметров приводят к вызову
тестируемого метода. Во-вторых, отсутствует непосредственный контроль
над параметрами тестового воздействия, а опосредованный контроль
может быть слишком труден или даже вовсе невозможен. И, наконец,
в-третьих, достаточно сложно определить, отработал ли тестируемый
метод должным образом.
Учитывая все сказанное, на практике используется
существенно более простое решение. Найденное описание класса
выносится в отдельный заголовочный файл, который поставляется вместе
с тестом. Вообще говоря, вынесение описания в отдельный файл
необходимо только для классов, использующих предоставляемые Qt
расширения языка C++, поскольку
прекомпилятор Meta Object
Compiler, который генерирует на
их основе определения на чистом C++,
обрабатывает только файлы заголовочного типа. Однако в целях
стандартизации процесса подготовки необходимых для создания теста
данных различий между классами, определенными на чистом языке, и
классами, использующими расширения, не делается. Теперь, при
подключении этого заголовочного файла к тесту, компилятор будет
считать исследуемые методы частью API, и к ним можно обращаться
напрямую, как и в общем случае. Данный подход также требует
некоторого количества ручной работы по поиску объявления класса, но
ее объем не идет ни в какое сравнение с предыдущим вариантом.
Также в состав LSB входит множество так называемых
thunk методов, которые конструируются компилятором и служат
для обращения к виртуальным методам, декларированным в
классах-предках. В силу их сугубо вспомогательной природы и
отсутствия документации по ним такие операции считаются не
подлежащими тестированию и исключаются из дальнейшего рассмотрения.
2.5. Среда исполнения тестируемого метода
Одним из ключевых компонентов приложения,
написанного с использованием библиотеки Qt, является объект класса
QApplication. Он
содержит основной цикл обработки сообщений и служит для глобальной
инициализации и финализации программы, в частности, если требуется,
обеспечивает приложению доступ к графической подсистеме, задавая
активный дисплей, визуальный и цветовой контексты. Также класс
QApplication
позволяет обращаться к таким параметрам системы как шрифты, палитра,
интервал двойного нажатия, и параметрам, переданным приложению.
Каждое приложение с графическим интерфейсом, использующее Qt, должно
иметь лишь один объект этого класса.
Система Qt не позволяет создавать объекты классов,
осуществляющих вывод какого-либо изображения, до тех пор, пока не
проинициализирован объект QApplication.
Существует также ряд методов, функциональность которых проявляется
только после того, как программа начнет обрабатывать события,
происходящие в ней самой и в операционной системе.
Поэтому компоновщик добавляет в начало каждого теста
конструктор класса QApplication,
а в конце теста вызывает метод exec полученного объекта,
который запускает цикл обработки сообщений данного приложения.
Из этого правила существует несколько исключений.
Поскольку в приложении может существовать лишь один объект класса
QApplication, то
при тестировании его конструкторов и деструкторов попытка создать
дополнительный объект приведет к падению теста. Класс QEventLoop
описывает основной цикл обработки сообщений, поэтому его объект
должен быть сконструирован до объекта QApplication.
Также, статический метод самого класса QApplication
setColorSpec() влияет на инициализацию графической
подсистемы и должен вызываться до начала ее работы, а значит до того,
как будет вызван конструктор класса QApplication.
Разумеется, многие классы, не затрагивающие
непосредственно графический интерфейс, могут работать и без
привлечения объекта QApplication,
поэтому дополнительная инициализация и вход в цикл обработки
сообщений для их тестирования излишни. Однако библиотека Qt
преимущественно используется для создания приложений, имеющих
графический интерфейс, поэтому подобная избыточность, напротив,
приближает среду, в которой вызывается тестируемая операция, к
реальной.
2.6. Сигналы и слоты
В библиотеке Qt передача сообщений между объектами
приложения осуществляется посредством механизма сигналов и слотов.
Вызов метода-сигнала объекта отправляет в основной цикл обработки
сообщение, содержащее параметры этого сигнала, которое
перехватывается и обрабатывается методом-слотом другого или того же
самого объекта. Канал связи между методами определяется макросом
connect.
Функциональность сигнала заключается в передаче
слоту параметров через сообщение, неявно включая указатель на
передающий объект, который может быть получен при помощи вызова
QObject::sender()
в теле слота. Именно ее и нужно проверять в процессе тестирования. В
тесте формируется дополнительный объект, имеющий метод-слот с такими
же, как и у сигнала, параметрами. Реализация этого метода проверяет
значения пришедших параметров на эквивалентность посланным, а также
неравенство нулю указателя на передающий сообщение объект, и
выполняет выход из приложения. Таким образом, если сообщение не дошло
до слота, то приложение не выйдет из основного цикла и будет
завершено аварийно.
Слот представляет собой обычный метод, работающий с
переданными ему параметрами, но может дополнительно обращаться к
объекту, пославшему сообщение, посредством глобальной переменной.
Поэтому не все реализации методов-слотов будут работать вне контекста
передачи сообщения. Для тестируемого слота подбирается существующий в
системе подходящий сигнал с таким же, как и у слота, набором
параметров. В тесте конструируется объект, имеющий этот метод-сигнал,
который при помощи макроса связывается со слотом. Тестовое
воздействие производится путем вызова сигнала с
проинициализированными параметрами.
2.7. Завершение теста и отложенное выполнение целевого воздействия
В подавляющем большинстве случаев приложение,
осуществляющее обработку сообщений, завершает свою работу по
получении некоторого внешнего сигнала. Например, приложение, имеющее
графический интерфейс, в отсутствие исключительных ситуаций работает
до тех пор, пока пользователь явно не даст команду на его закрытие.
Симуляция такого воздействия выходит за рамки
тестирования работоспособности, поэтому завершение работы
осуществляется тестом самостоятельно. При помощи сигнала singleShot
класса QTimer
через определенное время после запуска приложения вызывается слот
quit класса QApplication,
что и приводит к выходу из цикла обработки сообщений. Тесты
запускаются параллельно, поэтому значительных задержек из-за
относительно большого времени жизни каждого из них в самом процессе
тестирования не возникает.
Таким образом, если он завершился самостоятельно в
отведенные временные рамки, и при этом не возникло ошибок, то тест
считается прошедшим. Если же он не завершился в нужное время, то тест
уничтожается загрузчиком, и считается, что тест не прошел.
Объекты классов-наследников QDialog
имеют свой собственный локальный цикл обработки сообщений. Если
локальный цикл получает управление до того, как будет осуществлен
вход в основной цикл, то сигнал о завершении работы, испускаемый
самим приложением, не будет обработан. Возникает необходимость
воздействия на диалоговое окно со стороны внешнего источника. В
дополнение, некоторые методы не могут работать в основном режиме до
тех пор, пока не запущен механизм обработки сообщений приложения.
Поэтому возникает необходимость тестировать ряд операций уже после
того, как управление передано основному циклу.
Все содержимое теста, необходимое для вызова такой
целевой операции, переносится в метод-слот дополнительно
сгенерированного класса. С помощью сигнала singleShot
этот слот вызывается через некоторое время после
запуска приложения, но до его завершения.