Добавлен: 04.02.2024
Просмотров: 67
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
модуль.
Поскольку тестовое окружение само является программой (причем зачастую реализованной не на том языке программирования, на котором написана система), оно само должно быть протестировано. Целью тестирования тестового окружения является доказательство того, что тестовое окружение никаким образом не искажает выполнение тестируемого модуля и адекватно моделирует поведение системы.
3.3.1. Драйверы и заглушки
Тестовое окружение для программного кода на структурных языках программирования состоит из двух компонентов - драйвера, который обеспечивает запуск и выполнение тестируемого модуля, и заглушек, которые моделируют функции, вызываемые из данного модуля. Разработка тестового драйвера представляет собой отдельную задачу тестирования, сам драйвер должен быть протестирован, дабы исключить неверное тестирование. Драйвер и заглушки могут иметь различные уровни сложности, требуемый уровень сложности выбирается в зависимости от сложности тестируемого модуля и уровня тестирования. Так, драйвер может выполнять следующие функции:
Заглушки могут выполнять следующие функции:
Для тестирования программного кода, написанного на процедурном языке программирования, используются драйверы, представляющие собой программу с точкой входа (например, функцией main() ), функциями запуска тестируемого модуля и функциями сбора результатов. Обычно драйвер имеет как минимум одну функцию - точку входа, которой передается управление при его вызове.
Функции-заглушки могут помещаться в тот же файл исходного кода, что и основной текст драйвера. Имена и параметры заглушек должны совпадать с именами и параметрами "заглушаемых" функций реальной системы. Это требование важно не столько с точки зрения корректной сборки системы (при сборке тестового драйвера и тестируемого ПО может использоваться приведение типов), сколько для того, чтобы максимально точно моделировать поведение реальной системы по передаче данных. Так, например, если в реальной системе присутствует функция вычисления квадратного корня
double sqrt(double value);
то, с точки зрения сборки системы, вместо типа double может использоваться и float, но снижение точности может вызвать непредсказуемые результаты в тестируемом модуле.
В качестве примера драйвера и заглушек рассмотрим реализацию стека на языке C, причем значения, помещаемые в стек, хранятся не в оперативной памяти, а помещаются в ППЗУ при помощи отдельного модуля, содержащего две функции - записи данных в ППЗУ по адресу и чтения данных по адресу.
Формат этих функций следующий:
void NV_Read(char *destination, long length, long offset);
void NV_Write(char *source, long length, long offset);
Здесь destination - адрес области памяти, в которую записывается значение, считанное из ППЗУ, source - адрес области памяти, из которой записывается значение в ППЗУ, length - длина записываемой области памяти, offset - смещение относительно начального адреса ППЗУ.
Реализация стека с использованием этих функций выглядит следующим образом:
long currentOffset;
void initStack()
{
currentOffset=0;
}
void push(int value)
{
NV_Write((int*)&value,sizeof(int),currentOffset);
currentOffset+=sizeof(int);
}
int pop()
{
int value;
if (currentOffset>0)
{
NV_Read((int*)&value,sizeof(int),currentOffset;
currentOffset-=sizeof(int);
}
}
При выполнении этого кода на реальной системе происходит запись в ППЗУ, однако, если мы хотим протестировать только реализацию стека, изолировав ее от реализации модуля работы с ППЗУ, необходимо использовать заглушки вместо реальных функций. Для имитации работы ППЗУ можно выделить достаточно большой участок оперативной памяти, в которой и будет производиться запись данных, получаемых заглушкой.
Заглушки для функций могут выглядеть следующим образом:
char nvrom[1024];
void NV_Read(char *destination, long length, long offset)
{
printf("NV_Read called\n");
memcpy(destination, nvrom+offset, length);
}
void NV_Write(char *source, long length, long offset);
{
printf("NV_Write called\n");
memcpy(nvrom+offset, source, length);
}
Каждая из заглушек выводит трассировочное сообщение и перемещает переданное значение в память, эмулирующую ППЗУ (функция NV_Write ), или возвращает по ссылке значение, которое хранится в памяти, эмулирующей ППЗУ (функция NV_Read ).
Схема взаимодействия тестируемого ПО (функций работы со стеком) с реальным окружением (основной частью системы и модулем работы с ППЗУ) и тестовым окружением (драйвером и заглушками функций работы с ППЗУ) показана на Рис 3.2 и Рис 3.3.
увеличить изображение
Рис. 3.2. Схема взаимодействия частей реальной системы
Рис. 3.3. Схема взаимодействия тестового окружения и тестируемого ПО
3.3.2. Тестовые классы
Тестовое окружение для объектно-ориентированного ПО выполняет те же самые функции, что и для структурных программ (на процедурных языках). Однако, оно имеет некоторые особенности, связанные с применением наследования и инкапсуляции.
Если при тестировании структурных программ минимальным тестируемым объектом является функция, то в объектно-ориентированным ПО минимальным объектом является класс. При применении принципа инкапсуляции все внутренние данные класса и некоторая часть его методов недоступна извне. В этом случае тестировщик лишен возможности обращаться в своих тестах к данным класса и произвольным образом вызывать методы; единственное, что ему доступно - вызывать методы внешнего интерфейса класса.
Существует несколько подходов к тестированию классов, каждый из них накладывает свои ограничения на структуру драйвера и заглушек.
Основное достоинство первых двух методов: при их
использовании класс работает точно таким же образом, как в реальной системе. Однако в этом случае нельзя гарантировать, что в процессе тестирования будет выполнен весь программный код класса и не останется непротестированных методов.
Основной недостаток 3-го метода: после изменения исходных текстов тестируемого модуля нельзя дать гарантии того, что класс будет вести себя таким же образом, как и исходный. В частности это связано с тем, что изменение защиты данных класса влияет на наследование данных и методов другими классами.
Тестирование наследования - отдельная сложная задача в объектно-ориентированных системах. После того, как протестирован базовый класс, необходимо тестировать классы-потомки. Однако, для базового класса нельзя создавать заглушки, т.к. в этом случае можно пропустить возможные проблемы полиморфизма. Если класс-потомок использует методы базового класса для обработки собственных данных, необходимо убедиться в том, что эти методы работают.
Таким образом, иерархия классов может тестироваться сверху вниз, начиная от базового класса. Тестовое окружение при этом может меняться для каждой тестируемой конфигурации классов.
3.3.3. Генераторы сигналов (событийно-управляемый код)
Значительная часть сложных программ в настоящее время использует ту или иную форму межпроцессного взаимодействия. Это обусловлено естественной эволюцией подходов к проектированию программных систем, которая последовательно прошла следующие этапы [11].
При выполнении многих процессов, решающих общую задачу, используются несколько типичных механизмов взаимодействия между ними, направленных на решение следующих задач:
Во всех этих случаях типичная структура каждого процесса представляет собой конечный автомат с набором состояний и переходов между ними. Находясь в определенном состоянии, процесс выполняет обработку данных, при переходе между состояниями - пересылает данные другим процессам или принимает данные от них [12].
Для моделирования конечных автоматов используются stateflow [13] или SDL-диаграммы [13], акцент в которых делается соответственно на условиях перехода между состояниями и пересылаемыми данными.
Так, на Рис 3.4 показана схема процесса приема/передачи данных. Закругленными прямоугольниками указаны состояния процесса, тонкими стрелками - переходы между состояниями, большими стрелками - пересылаемые данные. Находясь в состоянии "Старт", процесс посылает во внешний мир (или процессу, с которым он обменивается данными) сообщение о своей готовности к началу сеанса передачи данных. После получения от второго процесса подтверждения о готовности начинается сеанс обмена данными. В случае поступления сообщения о конце данных происходит завершение сеанса и переход в стартовое состояние. В случае поступления неверных данных (например, неправильного формата или с неверной контрольной суммой) процесс переходит в состояние "Ошибка", выйти из которого возможно только завершением и перезапуском процесса.
Рис. 3.4. Пример конечного автомата процесса приема-передачи данных
Тестовое окружение для такого процесса также должно иметь структуру конечного автомата и пересылать данные в том же формате, что и тестируемый процесс. Целью тестирования в данном случае будет показать, что процесс обрабатывает принимаемые данные в соответствии с требованиями, форматы передаваемых данных корректны, а также что процесс во время своей работы действительно проходит все состояния конечного автомата, моделирующего его поведение.
Поскольку тестовое окружение само является программой (причем зачастую реализованной не на том языке программирования, на котором написана система), оно само должно быть протестировано. Целью тестирования тестового окружения является доказательство того, что тестовое окружение никаким образом не искажает выполнение тестируемого модуля и адекватно моделирует поведение системы.
3.3.1. Драйверы и заглушки
Тестовое окружение для программного кода на структурных языках программирования состоит из двух компонентов - драйвера, который обеспечивает запуск и выполнение тестируемого модуля, и заглушек, которые моделируют функции, вызываемые из данного модуля. Разработка тестового драйвера представляет собой отдельную задачу тестирования, сам драйвер должен быть протестирован, дабы исключить неверное тестирование. Драйвер и заглушки могут иметь различные уровни сложности, требуемый уровень сложности выбирается в зависимости от сложности тестируемого модуля и уровня тестирования. Так, драйвер может выполнять следующие функции:
-
Вызов тестируемого модуля -
1 + передача в тестируемый модуль входных значений и прием результатов -
2 + вывод выходных значений -
3 + протоколирование процесса тестирования и ключевых точек программы
Заглушки могут выполнять следующие функции:
-
Не производить никаких действий (такие заглушки нужны для корректной сборки тестируемого модуля) -
Выводить сообщения о том, что заглушка была вызвана -
1 + выводить сообщения со значениями параметров, переданных в функцию -
2 + возвращать значение, заранее заданное во входных параметрах теста -
3 + выводить значение, заранее заданное во входных параметрах теста -
3 + принимать от тестируемого ПО значения и передавать их в драйвер [10].
Для тестирования программного кода, написанного на процедурном языке программирования, используются драйверы, представляющие собой программу с точкой входа (например, функцией main() ), функциями запуска тестируемого модуля и функциями сбора результатов. Обычно драйвер имеет как минимум одну функцию - точку входа, которой передается управление при его вызове.
Функции-заглушки могут помещаться в тот же файл исходного кода, что и основной текст драйвера. Имена и параметры заглушек должны совпадать с именами и параметрами "заглушаемых" функций реальной системы. Это требование важно не столько с точки зрения корректной сборки системы (при сборке тестового драйвера и тестируемого ПО может использоваться приведение типов), сколько для того, чтобы максимально точно моделировать поведение реальной системы по передаче данных. Так, например, если в реальной системе присутствует функция вычисления квадратного корня
double sqrt(double value);
то, с точки зрения сборки системы, вместо типа double может использоваться и float, но снижение точности может вызвать непредсказуемые результаты в тестируемом модуле.
В качестве примера драйвера и заглушек рассмотрим реализацию стека на языке C, причем значения, помещаемые в стек, хранятся не в оперативной памяти, а помещаются в ППЗУ при помощи отдельного модуля, содержащего две функции - записи данных в ППЗУ по адресу и чтения данных по адресу.
Формат этих функций следующий:
void NV_Read(char *destination, long length, long offset);
void NV_Write(char *source, long length, long offset);
Здесь destination - адрес области памяти, в которую записывается значение, считанное из ППЗУ, source - адрес области памяти, из которой записывается значение в ППЗУ, length - длина записываемой области памяти, offset - смещение относительно начального адреса ППЗУ.
Реализация стека с использованием этих функций выглядит следующим образом:
long currentOffset;
void initStack()
{
currentOffset=0;
}
void push(int value)
{
NV_Write((int*)&value,sizeof(int),currentOffset);
currentOffset+=sizeof(int);
}
int pop()
{
int value;
if (currentOffset>0)
{
NV_Read((int*)&value,sizeof(int),currentOffset;
currentOffset-=sizeof(int);
}
}
При выполнении этого кода на реальной системе происходит запись в ППЗУ, однако, если мы хотим протестировать только реализацию стека, изолировав ее от реализации модуля работы с ППЗУ, необходимо использовать заглушки вместо реальных функций. Для имитации работы ППЗУ можно выделить достаточно большой участок оперативной памяти, в которой и будет производиться запись данных, получаемых заглушкой.
Заглушки для функций могут выглядеть следующим образом:
char nvrom[1024];
void NV_Read(char *destination, long length, long offset)
{
printf("NV_Read called\n");
memcpy(destination, nvrom+offset, length);
}
void NV_Write(char *source, long length, long offset);
{
printf("NV_Write called\n");
memcpy(nvrom+offset, source, length);
}
Каждая из заглушек выводит трассировочное сообщение и перемещает переданное значение в память, эмулирующую ППЗУ (функция NV_Write ), или возвращает по ссылке значение, которое хранится в памяти, эмулирующей ППЗУ (функция NV_Read ).
Схема взаимодействия тестируемого ПО (функций работы со стеком) с реальным окружением (основной частью системы и модулем работы с ППЗУ) и тестовым окружением (драйвером и заглушками функций работы с ППЗУ) показана на Рис 3.2 и Рис 3.3.
увеличить изображение
Рис. 3.2. Схема взаимодействия частей реальной системы
Рис. 3.3. Схема взаимодействия тестового окружения и тестируемого ПО
3.3.2. Тестовые классы
Тестовое окружение для объектно-ориентированного ПО выполняет те же самые функции, что и для структурных программ (на процедурных языках). Однако, оно имеет некоторые особенности, связанные с применением наследования и инкапсуляции.
Если при тестировании структурных программ минимальным тестируемым объектом является функция, то в объектно-ориентированным ПО минимальным объектом является класс. При применении принципа инкапсуляции все внутренние данные класса и некоторая часть его методов недоступна извне. В этом случае тестировщик лишен возможности обращаться в своих тестах к данным класса и произвольным образом вызывать методы; единственное, что ему доступно - вызывать методы внешнего интерфейса класса.
Существует несколько подходов к тестированию классов, каждый из них накладывает свои ограничения на структуру драйвера и заглушек.
-
Драйвер создает один или больше объектов тестируемого класса, все обращения к объектам происходят только с использованием их внешнего интерфейса. Текст драйвера в этом случае представляет собой т.н. тестирующий класс, который содержит по одному методу для каждого тестового примера. Процесс тестирования заключается в последовательном вызове этих методов. Вместо заглушек в состав тестового окружения входит программный код реальной системы, соответственно, отсутствует изоляция тестируемого класса. Однако, именно такой подход к тестированию принят сейчас в большинстве методологий и сред разработки. Его классическое название - unit testing (тестирование модулей), более подробно он будет рассматриваться позднее. -
Аналогично предыдущему подходу, но для всех классов, которые использует тестируемый класс, создаются заглушки -
Программный код тестируемого класса модифицируется таким образом, чтобы открыть доступ ко всем его свойствам и методам. Строение тестового окружения в этом случае полностью аналогично окружению для тестирования структурных программ. -
Используются специальные средства доступа к закрытым данным и методам класса на уровне объектного или исполняемого кода - скрипты отладчика или accessors в Visual Studio.
Основное достоинство первых двух методов: при их
использовании класс работает точно таким же образом, как в реальной системе. Однако в этом случае нельзя гарантировать, что в процессе тестирования будет выполнен весь программный код класса и не останется непротестированных методов.
Основной недостаток 3-го метода: после изменения исходных текстов тестируемого модуля нельзя дать гарантии того, что класс будет вести себя таким же образом, как и исходный. В частности это связано с тем, что изменение защиты данных класса влияет на наследование данных и методов другими классами.
Тестирование наследования - отдельная сложная задача в объектно-ориентированных системах. После того, как протестирован базовый класс, необходимо тестировать классы-потомки. Однако, для базового класса нельзя создавать заглушки, т.к. в этом случае можно пропустить возможные проблемы полиморфизма. Если класс-потомок использует методы базового класса для обработки собственных данных, необходимо убедиться в том, что эти методы работают.
Таким образом, иерархия классов может тестироваться сверху вниз, начиная от базового класса. Тестовое окружение при этом может меняться для каждой тестируемой конфигурации классов.
3.3.3. Генераторы сигналов (событийно-управляемый код)
Значительная часть сложных программ в настоящее время использует ту или иную форму межпроцессного взаимодействия. Это обусловлено естественной эволюцией подходов к проектированию программных систем, которая последовательно прошла следующие этапы [11].
-
Монолитные программы, содержащие в своем коде все необходимые для своей работы инструкции. Обмен данными внутри таких программ производится при помощи передачи параметров функций и использования глобальных переменных. При запуске таких программ образуется один процесс, который выполняет всю необходимую работу. -
Модульные программы, которые состоят из отдельных программных модулей с четко определенными интерфейсами вызовов. Объединение модулей в программу может происходить либо на этапе сборки исполняемого файла (статическая сборка или static linking ), либо на этапе выполнения программы (динамическая сборка или dynamic linking ). Преимущество модульных программ заключается в достижении некоторого уровня универсальности - один модуль может быть заменен другим. Однако, модульная программа все равно представляет собой один процесс, а данные, необходимые для решения задачи, передаются внутри процесса как параметры функций. -
Программы, использующие межпроцессное взаимодействие. Такие программы образуют программный комплекс, предназначенный для решения общей задачи. Каждая запущенная программа образует один или более процессов. Каждый из процессов либо использует для решения задачи свои собственные данные и обменивается с другими процессами только результатом своей работы, либо работает с общей областью данных, разделяемых между разными процессами. Для решения особо сложных задач процессы могут быть запущены на разных физических компьютерах и взаимодействовать через сеть. Преимущество использования межпроцессного взаимодействия заключается в еще большей универсальности - взаимодействующие процессы могут быть заменены независимо друг от друга при сохранении интерфейса взаимодействия. Другое преимущество состоит в том, что вычислительная нагрузка распределяется между процессами. Это позволяет операционной системе управлять приоритетами выполнения отдельных частей программного комплекса, выделяя большее или меньшее количество ресурсов ресурсоемким процессам.
При выполнении многих процессов, решающих общую задачу, используются несколько типичных механизмов взаимодействия между ними, направленных на решение следующих задач:
-
передача данных от одного процесса к другому; -
совместное использование одних и тех же данных несколькими процессами; -
извещения об изменении состояния процессов.
Во всех этих случаях типичная структура каждого процесса представляет собой конечный автомат с набором состояний и переходов между ними. Находясь в определенном состоянии, процесс выполняет обработку данных, при переходе между состояниями - пересылает данные другим процессам или принимает данные от них [12].
Для моделирования конечных автоматов используются stateflow [13] или SDL-диаграммы [13], акцент в которых делается соответственно на условиях перехода между состояниями и пересылаемыми данными.
Так, на Рис 3.4 показана схема процесса приема/передачи данных. Закругленными прямоугольниками указаны состояния процесса, тонкими стрелками - переходы между состояниями, большими стрелками - пересылаемые данные. Находясь в состоянии "Старт", процесс посылает во внешний мир (или процессу, с которым он обменивается данными) сообщение о своей готовности к началу сеанса передачи данных. После получения от второго процесса подтверждения о готовности начинается сеанс обмена данными. В случае поступления сообщения о конце данных происходит завершение сеанса и переход в стартовое состояние. В случае поступления неверных данных (например, неправильного формата или с неверной контрольной суммой) процесс переходит в состояние "Ошибка", выйти из которого возможно только завершением и перезапуском процесса.
Рис. 3.4. Пример конечного автомата процесса приема-передачи данных
Тестовое окружение для такого процесса также должно иметь структуру конечного автомата и пересылать данные в том же формате, что и тестируемый процесс. Целью тестирования в данном случае будет показать, что процесс обрабатывает принимаемые данные в соответствии с требованиями, форматы передаваемых данных корректны, а также что процесс во время своей работы действительно проходит все состояния конечного автомата, моделирующего его поведение.