Концепция тестирования
Программа – это аналог формулы в обычной математике.
Формула для функции f, полученной суперпозицией функций f1, f2, ... fn – выражение, описывающее эту суперпозицию.
f = f1* f2* f3*... * fnЕсли аналог f1,f2,... fn – операторы языка программирования, то их формула – программа.
Существует два метода обоснования истинности формул:
- Формальный подход или доказательство применяется, когда из исходных формул-аксиом с помощью формальных процедур (правил вывода) выводятся искомые формулы и утверждения (теоремы). Вывод осуществляется путем перехода от одних формул к другим по строгим правилам, которые позволяют свести процедуру перехода от формулы к формуле к последовательности текстовых подстановок: A**3 = A*A*A A*A*A = A -> R, A*R -> R, A*R -> R Преимущество формального подхода заключается в том, что с его помощью удается избегать обращений к бесконечной области значений и на каждом шаге доказательства оперировать только конечным множеством символов.
-
Интерпретационный подход применяется, когда осуществляется подстановка констант в формулы, а затем интерпретация формул, как осмысленных утверждений в элементах множеств конкретных значений. Истинность интерпретируемых формул проверяется на конечных множествах возможных значений. Сложность подхода состоит в том, что на конечных множествах комбинации возможных значений для реализации исчерпывающей проверки могут оказаться достаточно велики.Интерпретационный подход используется при экспериментальной проверке соответствия программы своей спецификации
Применение интерпретационного подхода в форме экспериментов над исполняемой программой составляет суть отладки и тестирования.
Организация тестирования
Тестирование осуществляется на заданном заранее множестве входных данных X и множестве предполагаемых результатов Y – (X,Y), которые задают график желаемой функции. Кроме того, зафиксирована процедура Оракул (oracle), которая определяет, соответствуют ли выходные данные – Yв (вычисленные по входным данным – X) желаемым результатам – Y, т.е. принадлежит ли каждая вычисленная точка (x,yв) графику желаемой функции (X,Y).
Оракул дает заключение о факте появления неправильной пары (x,yв) и ничего не говорит о том, каким образом она была вычислена или каков правильный алгоритм – он только сравнивает вычисленные и желаемые результаты. Оракулом может быть даже Заказчик или программист, производящий соответствующие вычисления в уме, поскольку Оракулу нужен какой-либо альтернативный способ получения функции (X,Y) для вычисления эталонных значений Y.
Основная терминология
Отладка (debug, debugging)– процесс поиска, локализации и исправления ошибок в программе [9] [IEEE Std.610-12.1990].
Термин "отладка" в отечественной литературе используется двояко: для обозначения активности по поиску ошибок (собственно тестирование), по нахождению причин их появления и исправлению, или активности по локализации и исправлению ошибок.
Тестирование обеспечивает выявление (констатацию наличия) фактов расхождений с требованиями (ошибок).
Как правило, на фазе тестирования осуществляется и исправление идентифицированных ошибок, включающее локализацию ошибок, нахождение причин ошибок и соответствующую корректировку программы тестируемого приложения (Application Under Testing (AUT) или Implementation Under Testing (IUT)).
Если программа не содержит синтаксических ошибок (прошла трансляцию) и может быть выполнена на компьютере, она обязательно вычисляет какую-либо функцию, осуществляющую отображение входных данных в выходные. Это означает, что компьютер на своих ресурсах доопределяет частично определенную программой функцию до тотальной определенности. Следовательно, судить о правильности или неправильности результатов выполнения программы можно, только сравнивая спецификацию желаемой функции с результатами ее вычисления, что и осуществляется в процессе тестирования.
Основные проблемы тестирования
Рассмотрим два примера тестирования:
- Пусть программа H(x:int, y:int) реализована в машине с 64 разрядным словами, тогда мощность множества тестов ||(X,Y)||=2**64Это означает, что компьютеру, работающему на частоте 1Ггц, для прогона этого набора тестов (при условии, что один тест выполняется за 100 команд) потребуется ~ 3K лет.
- На Рис. 2.3 приведен фрагмент схемы программы управления схватом робота, где интервал между моментами срабатывания схвата не определен.Этот тривиальный пример требует прогона бесконечного множества последовательностей входных значений с разными интервалами срабатывания схвата (Пример 2.8).

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

Пример обратного выполнения для программы вычисления степени числа x
В программе на Пример 2.4 фиксируются значения всех переменных после выполнения каждого оператора.
// Метод вычисляет неотрицательную // степень n числа x static public double PowerNonNeg(double x, int n) { double z=1; Console.WriteLine("x={0} z={1} n={2}", x,z,n); if (n>0) { Console.WriteLine("x={0} z={1} n={2}", x,z,n); for (int i=1;n>=i;i++) { z = z*x; Console.WriteLine( "x={0} z={1} n={2}" + " i={3}",x,z,n,i); } } else Console.WriteLine( "Ошибка ! Степень" + " числа n должна быть больше 0."); return z; } Пример 2.4. Исходный код с фиксацией результатов выполнения операторовdouble PowerNonNeg(double x, int n) { double z=1; int i; printf("x=%f z=%f n=%d\n",x,z,n); if (n>0) { printf("x=%f z=%f n=%d\n",x,z,n); for (i=1;n>=i;i++) { z = z*x; printf("x=%f z=%f n=%d i=%d\n", x,z,n,i); } } else printf( "Ошибка ! Степень " "числа n должна быть больше 0.\n"); return z; } Пример 2.4.1. Исходный код с фиксацией результатов выполнения операторовЗная структуру управляющего графа программы и имея значения всех переменных после выполнения каждого оператора, можно осуществить обратное выполнение (например, в уме), подставляя значения переменных в операторы и двигаясь снизу вверх, начиная с последнего.
Итак, в процессе тестирования сравнение промежуточных результатов с полученными независимо эталонными результатами позволяет найти причины и место ошибки, исправить текст программы, провести повторную трансляцию и настройку на выполнение и продолжить тестирование.
Тестирование заканчивается, когда выполнилось или "прошло" (pass) успешно достаточное количество тестов в соответствии с выбранным критерием тестирования.
Тестирование – это:
- Процесс выполнения ПО системы или компонента в условиях анализа или записи получаемых результатов с целью проверки (оценки) некоторых свойств тестируемого объекта.The process of operating a system or component under specified conditions, observing or recording the results, and making an evaluation of some aspect of the system or component [9].
- Процесс анализа пункта требований к ПО с целью фиксации различий между существующим состоянием ПО и требуемым (что свидетельствует о проявлении ошибки) при экспериментальной проверке соответствующего пункта требований.The process of analyzing a software item to detect the differences between existing and required conditions (that is, bugs) and to evaluate features of software items [[IEEE Std.610-12.1990], [9].
- Контролируемое выполнение программы на конечном множестве тестовых данных и анализ результатов этого выполнения для поиска ошибок [IEEE Std 829-1983].
Пример поиска и исправления ошибки
Отладка обеспечивает локализацию ошибок, поиск причин ошибок и соответствующую корректировку программы (Пример 2.1, Пример 2.2).
// Метод вычисляет неотрицательную // степень n числа x static public double Power(double x, int n) { double z=1; for (int i=1;n>=i;i++) { z = z*x; } return z; } Пример 2.1. Исходный текст метода Powerdouble Power(double x,int n) { double z=1; int i; for(i=1;n>=i;i++) { z=z*x; } return z; } Пример 2.1.1. Исходный текст метода PowerЕсли вызвать метод Power с отрицательным значением степени n Power(2,-1), то получим некорректный результат - 2. Исправим метод так, чтобы ошибочное значение параметра (недопустимое по спецификации значение) идентифицировалось специальным сообщением, а возвращаемый результат был равен 1 (Пример 2.2).
// Метод вычисляет неотрицательную // степень n числа x static public double PowerNonNeg(double x, int n) { double z=1; if (n>0) { for (int i=1;n>=i;i++) { z = z*x; } } else Console.WriteLine( "Ошибка ! Степень числа n" + " должна быть больше 0."); return z; } Пример 2.2. Скорректированный исходный текстdouble PowerNonNeg(double x, int n) { double z=1; int i; if (n>0) { for (i=1;n>=i;i++) { z = z*x; } } else printf("Ошибка! Степень числа n должна быть больше 0.\n"); return z; } Пример 2.2.1. Скорректированный исходный текстЕсли вызвать скорректированный метод PowerNonNeg(2,-1) с отрицательным значением параметра степени, то сообщение об ошибке будет выдано автоматически.
Судить о правильности или неправильности результатов выполнения программы можно только сравнивая спецификацию желаемой функции с результатами ее вычисления.
Тестирование разделяют на статическое и динамическое:
Статическое тестирование выявляет формальными методами анализа без выполнения тестируемой программы неверные конструкции или неверные отношения объектов программы (ошибки формального задания) с помощью специальных инструментов контроля кода – CodeChecker.
Динамическое тестирование (собственно тестирование) осуществляет выявление ошибок только на выполняющейся программе с помощью специальных инструментов автоматизации тестирования – Testbed [9] или Testbench.
Пример пошагового выполнения программы
При пошаговом выполнении программы код выполняется строчка за строчкой. В среде Microsoft Visual Studio.NET возможны следующие команды пошагового выполнения:
- Step Into – если выполняемая строчка кода содержит вызов функции, процедуры или метода, то происходит вызов, и программа останавливается на первой строчке вызываемой функции, процедуры или метода.
- Step Over - если выполняемая строчка кода содержит вызов функции, процедуры или метода, то происходит вызов и выполнение всей функции и программа останавливается на первой строчке после вызываемой функции.
- Step Out – предназначена для выхода из функции в вызывающую функцию. Эта команда продолжит выполнение функции и остановит выполнение на первой строчке после вызываемой функции.
- Выполнение с заказанными остановками (breakpoints), анализом трасс (traces) или состояний памяти - дампов (dump).
Пример сравнения словесного описания пункта спецификации с результатом выполнения фрагмента кода
Пункт спецификации: "Метод Power должен принимать входные параметры: x – целое число, возводимое в степень, и n – неотрицательный порядок степени. Метод должен возвращать вычисленное значение xn".
Выполняем метод со следующими параметрами: Power(2,2)
Проверка результата выполнения возможна, когда результат вычисления заранее известен – 4. Если результат выполнения 22 = 4, то он соответствует спецификации.
В процессе тестирования Оракул последовательно получает элементы множества (X,Y) и соответствующие им результаты вычислений (X,Yв) для идентификации фактов несовпадений (test incident).
При выявлении (x,yв)

- "Выполнение программы в уме" (deskchecking).
- Вставка операторов протоколирования (печати) промежуточных результатов (logging).
Пример вставки операторов протоколирования промежуточных результатов
Можно выводить промежуточные значения переменных при выполнении программы. Код, осуществляющий вывод, помечен светлым тоном (Пример 2.3). Этот метод относится к наиболее популярным средствам автоматизации отладки программистов прошлых десятилетий. В настоящее время он известен как метод внедрения "агентов" в текст отлаживаемой программы.
// Метод вычисляет неотрицательную // степень n числа x static public double Power(double x, int n) { double z=1; for (int i=1;n>=i;i++) { z = z*x; Console.WriteLine("i = {0} z = {1}", i, z); } return z; } Пример 2.3. Исходный текст метода Power со вставкой оператора протоколированияdouble Power(double x, int n) { double z=1; int i; for (i=1;n>=i;i++) { z = z*x; printf("i = %d z = %f\n",i,z); } return z; } Пример 2.3.1. Исходный текст метода Power со вставкой оператора протоколирования
- Пошаговое выполнение программы (single-step running).
Пример выполнения программы с заказанными контрольными точками и анализом трасс и дампов
- Контрольная точка (breakpoint)– точка программы, которая при ее достижении посылает отладчику сигнал. По этому сигналу либо временно приостанавливается выполнение отлаживаемой программы, либо запускается программа "агент", фиксирующая состояние заранее определенных переменных или областей в данный момент.
- Когда выполнение в контрольной точке приостанавливается, отлаживаемая программа переходит в режим останова (break mode). Вход в режим останова не прерывает и не заканчивает выполнение программы и позволяет анализировать состояние отдельных переменных или структур данных. Возврат из режима break mode в режим выполнения может произойти в любой момент по желанию пользователя.
- Когда в контрольной точке вызывается программа "агент", она тоже приостанавливает выполнение отлаживаемой программы, но только на время, необходимое для фиксации состояния выбранных переменных или структур данных в специальном электронном журнале - Log-файле, после чего происходит автоматический возврат в режим исполнения.
- Трасса - это "сохраненный путь" на управляющем графе программы, т.е. зафиксированные в журнале записи о состояниях переменных в заданных точках в ходе выполнения программы.Например: на Рис. 2.1 условно изображен управляющий граф некоторой программы. Трасса, проходящая через вершины 0-1-3-4-5 зафиксирована в Табл. 2.1. Строки таблицы отображают вершины управляющего графа программы, или breakpoints, в которых фиксировались текущие значения заказанных пользователем переменных.

Рис. 2.1. Управляющий граф программыТаблица 2.1. Трасса, проходящая через вершины 0-1-3-4-5
№ вершины-оператораЗначение переменной xЗначение переменной zЗначение переменной nЗначение переменной i 0 3 1 2 не зафиксировано 1 3 1 2 не зафиксировано 3 3 1 2 1 4 3 3 2 2 5 3 3 2 не зафиксировано - Дамп– область памяти, состояние которой фиксируется в контрольной точке в виде единого массива или нескольких связанных массивов. При анализе, который осуществляется после выполнения трассы в режиме off-line, состояния дампа структурируются, и выделенные области или поля сравниваются с состояниями, предусмотренными спецификацией. Например, при моделировании поведения управляющих программ контроллеров в виде дампа фиксируются области общих и специальных регистров, или целые области оперативной памяти, состояния которой определяет алгоритм управления внешней средой.
- реверсивное (обратное) выполнение (reversible execution)
Простой пример
Рассмотрим вопросы тестирования на примере простой программы (Пример 2.6) на языке С#. Текст этой программы и некоторых других несколько видоизменен с целью сделать иллюстрацию описываемых фактов более прозрачной.
/* Функция вычисляет неотрицательную степень n числа x */ 1 double Power(double x, int n){ 2 double z=1; int i; 3 for (i=1; 4 n>=i; 5 i++) 6 {z = z*x;} /* Возврат в п.4 */ 7 return z;} Пример 2.6. Пример простой программы на языке С#/* Функция вычисляет неотрицательную степень n числа x */ 1 double Power(double x, int n){ 2 double z=1; int i; 3 for (i=1; 4 n>=i; 5 i++) 6 {z = z*x;} /* Возврат в п.4 */ 7 return z;} Пример 2.6.1. Пример простой программы на языке С

Рис. 2.2. Управляющий граф программы
Управляющий граф программы (УГП) на Рис. 2.2 отображает поток управления программы. Нумерация узлов графа совпадает с нумерацией строк программы. Узлы 1 и 2 не включаются в УГП, поскольку отображают строки описаний, т.е. не содержат управляющих операторов.
Сквозной пример тестирования
Возьмем несколько отличающуюся от Пример 2.4 программу:
// Метод вычисляет степень n числа x static public double Power(int x, int n) { int z=1; for (int i=1;n>=i;i++) { z = z*x; } return z; } [STAThread] static void Main(string[] args) { int x; int n; try { Console.WriteLine("Enter x:"); x=Convert.ToInt32(Console.ReadLine()); if ((x>=0) & (x<=999)) { Console.WriteLine("Enter n:"); n=Convert.ToInt32(Console.ReadLine()); if ((n>=1) & (n<=100)) { Console.WriteLine("The power n" + " of x is {0}", Power(x,n)); Console.ReadLine(); } else { Console.WriteLine("Error : n " + "must be in [1..100]"); Console.ReadLine(); } } else { Console.WriteLine("Error : x " + "must be in [0..999]"); Console.ReadLine(); } } catch (Exception e) { Console.WriteLine("Error : Please enter " + "a numeric argument."); Console.ReadLine(); } } Пример 2.5. Другой пример вычисления степени числа#include
Спецификация программы
На вход программа принимает два параметра: x - число, n – степень. Результат вычисления выводится на консоль.
Значения числа и степени должны быть целыми.
Значения числа, возводимого в степень, должны лежать в диапазоне – [0..999].
Значения степени должны лежать в диапазоне – [1..100].
Если числа, подаваемые на вход, лежат за пределами указанных диапазонов, то должно выдаваться сообщение об ошибке.
Разработка тестов
Определим области эквивалентности входных параметров.
Для x – числа, возводимого в степень, определим классы возможных значений:
- x < 0 (ошибочное)
- x > 999 (ошибочное)
- x - не число (ошибочное)
-
0 <= x <= 999(корректное)Для n – степени числа:
- n < 1 (ошибочное)
- n > 100 (ошибочное)
- n - не число (ошибочное)
- 1 <= n <= 100 (корректное)
- Входные значения: (x = 2, n = 3) (покрывают классы 4, 8).Ожидаемый результат: The power n of x is 8.
- Входные значения: {(x = -1, n = 2),(x = 1000, n = 5 )} (покрывают классы 1, 2).Ожидаемый результат: Error : x must be in [0..999].
- Входные значения: {(x = 100, n = 0),(x = 100, n = 200)} (покрывают классы 5,6).Ожидаемый результат: Error : n must be in [1..100].
- Входные значения:(x = ADS n = ASD) (покрывают классы эквивалентности 3, 7).Ожидаемый результат: Error : Please enter a numeric argument.
- Проверка на граничные значения:
- Входные значения: (x = 999 n = 1).Ожидаемый результат: The power n of x is 999.
- Входные значения: x = 0 n = 100.Ожидаемый результат: The power n of x is 0.
- Входные значения: (x = 999 n = 1).Ожидаемый результат: The power n of x is 999.
Запустим программу с заданными значениями аргументов.
Оценка результатов выполнения программы на тестах
В процессе тестирования Оракул последовательно получает элементы множества (X,Y) и соответствующие им результаты вычислений YВ. В процессе тестирования производится оценка результатов выполнения путем сравнения получаемого результата с ожидаемым.
Три фазы тестирования
Реализация тестирования разделяется на три этапа:
- Создание тестового набора (test suite) путем ручной разработки или автоматической генерации для конкретной среды тестирования (testing environment).
- Прогон программы на тестах, управляемый тестовым монитором (test monitor, test driver [IEEE Std 829-1983], [9]) с получением протокола результатов тестирования (test log).
- Оценка результатов выполнения программы на наборе тестов с целью принятия решения о продолжении или остановке тестирования.
Управляющий граф программы
Управляющий граф программы (УГП)– граф G(V,A), где V(V1,… Vm) – множество вершин (операторов), A(A1,… An) – множество дуг (управлений), соединяющих операторы-вершины.
Путь– последовательность вершин и дуг УГП, в которой любая дуга выходит из вершины Vi и приходит в вершину Vj , например: (3,4,7), (3,4,5,6,4,5,6), (3,4), (3,4,5,6)
Ветвь– путь(V1, V2, … Vk), где V1 - либо первый, либо условный оператор программы, Vk - либо условный оператор, либо оператор выхода из программы, а все остальные операторы – безусловные, например: (3,4) (4,5,6,4) (4,7). Пути, различающиеся хотя бы числом прохождений цикла – разные пути, поэтому число путей в программе может быть не ограничено. Ветви - линейные участки программы, их конечноe число.
Существуют реализуемые и нереализуемые пути в программе, в нереализуемые пути в обычных условиях попасть нельзя.
float H(float x,float y) { float H; 1 if (x*x+y*y+2<=0) 2 H = 17; 3 else H = 64; 4 return H*H+x*x; } Пример 2.7. Пример описания функции с реализуемыми и нереализуемыми путямиfloat H(float x,float y) { float H; 1 if (x*x+y*y+2<=0) 2 H = 17; 3 else H = 64; 4 return H*H+x*x; } Пример 2.7.1. Пример описания функции с реализуемыми и нереализуемыми путямиНапример, для функции Пример 2.7 путь(1,3,4) реализуем, путь(1,2,4) нереализуем в условиях нормальной работы. Но при сбоях даже нереализуемый путь может реализоваться.
Требования к идеальному критерию тестирования
Функциональные критерии (класс II)
Функциональный критерий - важнейший для программной индустрии критерий тестирования. Он обеспечивает, прежде всего, контроль степени выполнения требований заказчика в программном продукте. Поскольку требования формулируются к продукту в целом, они отражают взаимодействие тестируемого приложения с окружением. При функциональном тестировании преимущественно используется модель "черного ящика". Проблема функционального тестирования - это, прежде всего, трудоемкость; дело в том, что документы, фиксирующие требования к программному изделию (Software requirement specification, Functional specification и т.п.), как правило, достаточно объемны, тем не менее, соответствующая проверка должна быть всеобъемлющей.
Ниже приведены частные виды функциональных критериев.
- Тестирование пунктов спецификации - набор тестов в совокупности должен обеспечить проверку каждого тестируемого пункта не менее одного раза.
Спецификация требований может содержать сотни и тысячи пунктов требований к программному продукту и каждое из этих требований при тестировании должно быть проверено в соответствии с критерием не менее чем одним тестом
- Тестирование классов входных данных - набор тестов в совокупности должен обеспечить проверку представителя каждого класса входных данных не менее одного раза.
При создании тестов классы входных данных сопоставляются с режимами использования тестируемого компонента или подсистемы приложения, что заметно сокращает варианты перебора, учитываемые при разработке тестовых наборов. Следует заметить, что перебирая в соответствии с критерием величины входных переменных (например, различные файлы - источники входных данных), мы вынуждены применять мощные тестовые наборы. Действительно, наряду с ограничениями на величины входных данных, существуют ограничения на величины входных данных во всевозможных комбинациях, в том числе проверка реакций системы на появление ошибок в значениях или структурах входных данных. Учет этого многообразия - процесс трудоемкий, что создает сложности для применения критерия
- Тестирование правил - набор тестов в совокупности должен обеспечить проверку каждого правила, если входные и выходные значения описываются набором правил, некоторой грамматики.
Следует заметить, что грамматика должна быть достаточно простой, чтобы трудоемкость разработки соответствующего набора тестов была реальной (вписывалась в сроки и штат специалистов, выделенных для реализации фазы тестирования)
- Тестирование классов выходных данных - набор тестов в совокупности должен обеспечить проверку представителя каждого выходного класса, при условии, что выходные результаты заранее расклассифицированы, причем отдельные классы результатов учитывают, в том числе, ограничения на ресурсы или на время (time out).
При создании тестов классы выходных данных сопоставляются с режимами использования тестируемого компонента или подсистемы, что заметно сокращает варианты перебора, учитываемые при разработке тестовых наборов.
- Тестирование функций - набор тестов в совокупности должен обеспечить проверку каждого действия, реализуемого тестируемым модулем, не менее одного раза.
Очень популярный на практике критерий, который, однако, не обеспечивает покрытия части функциональности тестируемого компонента, связанной со структурными и поведенческими свойствами, описание которых не сосредоточено в отдельных функциях (т.е. описание рассредоточено по компоненту).
Критерий тестирования функций объединяет отчасти особенности структурных и функциональных критериев. Он базируется на модели "полупрозрачного ящика", где явно указаны не только входы и выходы тестируемого компонента, но также состав и структура используемых методов (функций, процедур) и классов.
- Комбинированные критерии для программ и спецификаций - набор тестов в совокупности должен обеспечить проверку всех комбинаций непротиворечивых условий программ и спецификаций не менее одного раза.
При этом все комбинации непротиворечивых условий надо подтвердить, а условия противоречий следует обнаружить и ликвидировать.
Классы критериев
- Структурные критерии используют информацию о структуре программы (критерии так называемого "белого ящика")
- Функциональные критерии формулируются в описании требований к программному изделию (критерии так называемого "черного ящика")
- Критерии стохастического тестирования формулируются в терминах проверки наличия заданных свойств у тестируемого приложения, средствами проверки некоторой статистической гипотезы.
- Мутационные критерии ориентированы на проверку свойств программного изделия на основе подхода Монте-Карло.
Мутационный критерий (класс IV).
Постулируется, что профессиональные программисты пишут сразу почти правильные программы, отличающиеся от правильных мелкими ошибками или описками типа - перестановка местами максимальных значений индексов в описании массивов, ошибки в знаках арифметических операций, занижение или завышение границы цикла на 1 и т.п. Предлагается подход, позволяющий на основе мелких ошибок оценить общее число ошибок, оставшихся в программе.
Подход базируется на следующих понятиях:
Мутации - мелкие ошибки в программе.
Мутанты - программы, отличающиеся друг от друга мутациями.
Метод мутационного тестирования - в разрабатываемую программу P вносят мутации, т.е. искусственно создают программы-мутанты P1, P2... Затем программа P и ее мутанты тестируются на одном и том же наборе тестов (X,Y).
Если на наборе (X,Y) подтверждается правильность программы P и, кроме того, выявляются все внесенные в программы-мутанты ошибки, то набор тестов (X,Y) соответствует мутационному критерию, а тестируемая программа объявляется правильной.
Если некоторые мутанты не выявили всех мутаций, то надо расширять набор тестов (X,Y) и продолжать тестирование.
Пример применения функциональных
Пусть для решения задачи тестирования системы "Система управления автоматизированным комплексом хранения подшипников" (см. Приложение 1, FS) был разработан следующий фрагмент спецификации требований:
- Произвести опрос статуса склада (вызвать функцию GetStoreStat). Добавить в журнал сообщений запись "СИСТЕМА : Запрошен статус СКЛАДА". В зависимости от полученного значения произвести следующие действия:
- Полученный статус склада = 32. В приемную ячейку склада поступил подшипник. Система должна:
- Добавить в журнал сообщений запись "СКЛАД : Статус СКЛАДА = 32".
- Получить параметры поступившего подшипника с терминала подшипника (должна быть вызвана функция GetRollerPar).
- Добавить в журнал сообщений запись "СИСТЕМА: Запрошены параметры подшипника".
- В зависимости от возвращенного функцией GetRollerPar значения должны быть выполнены следующие действия (таблица 3.2):
Таблица 3.2. Действия по результатам функции GetRollerPar
Значение, возвращенное функцией GetRollerParДействия системы ... ... 0 - Добавить на первое место команду GetR - "ПОЛУЧИТЬ ИЗ ПРИЕМНИКА В ЯЧЕЙКУ"
- Добавить в журнал сообщений запись "ТЕРМИНАЛ ПОДШИПНИКА: 0 - параметры возвращены <Номер_группы>"
1 Добавить в журнал сообщений запись "ТЕРМИНАЛ ПОДШИПНИКА: 1 - нет данных" ... ... - Полученный статус склада = 32. В приемную ячейку склада поступил подшипник. Система должна:
- Произвести опрос терминала оси (вызвать функцию получения сообщения от терминала - GetAxlePar). В журнал сообщений должно быть добавлено сообщение "СИСТЕМА : Запрошены параметры оси". В зависимости от возвращенного функцией GetAxlePar значения должны быть выполнены следующие действия (таблица 3.3):
Таблица 3.3. Действия по результатам функции GetAxlePar
Значение, возвращенное функцией GetAxleParДействия системы ... ... 1 Добавить в журнал сообщений запись "ТЕРМИНАЛ ОСИ: 1 - нет данных" ... ...
- Статус склада = 0 (правильный).
- Статус склада = 4 (правильный).
- Статус склада = 16 (правильный).
- Статус склада = 32 (правильный).
- Статус склада = любое другое значение (ошибочный).
- Тестовый случай 1 (покрывает класс 4):
Состояние окружения (входные данные - X ):
Статус склада - 32.
...
Ожидаемая последовательность событий (выходные данные - Y):
Система запрашивает статус склада (вызов функции GetStoreStat) и получает 32
...
-
Тестовый случай 2 (покрывает класс 5):
Состояние окружения (входные данные - X):
Статус склада - 12dfga.
...
Ожидаемая последовательность событий (выходные данные - Y):
Система запрашивает статус склада (вызов функции GetStoreStat) и согласно пункту спецификации при ошибочном значении статуса склада в журнал добавляется сообщение "СКЛАД : ОШИБКА : Неопределенный статус".
...
Пример применения мутационного критерия
Тестируемая программа P приведена на пример 3.2. Для нее создается две программы-мутанта P1 и P2.
В P1 изменено начальное значение переменной z с 1 на 2 (пример 3.3).
В P2 изменено начальное значение переменной i с 1 на 0 и граничное значение индекса цикла с n на n-1 (пример 3.4).
При запуске тестов (X,Y) = {(x=2,n=3,y=8),(x=999,n=1,y=999), (x=0,n=100,y=0 } выявляются все ошибки в программах-мутантах и ошибка в основной программе, где в условии цикла вместо n стоит n-1:
// Метод вычисляет неотрицательную // степень n числа x static public double PowerNonNeg( double x, int n) { double z=1; if (n>0) { for (int i=1;n-1>=i;i++) { z = z*x; } } else Console.WriteLine( "Ошибка ! Степень числа n должна быть больше 0."); return z; } Пример 3.2. Основная программа P double PowerNonNeg(double x, int n) { double z=1; int i; if (n>0) { for (i=1;n-1>=i;i++) { z = z*x; } } else printf( "Ошибка ! Степень числа n должна быть больше 0.\n"); return z; } Пример 3.2.1. Основная программа PИзмененное начальное значение переменной z в мутанте Р1 помечено светлым тоном:
// Метод вычисляет неотрицательную // степень n числа x static public double PowerMutant1( double x, int n) { double z=2; if (n>0) { for (int i=1;n>=i;i++) { z = z*x; } } else Console.WriteLine( "Ошибка ! Степень числа n должна быть больше 0."); return z; } Пример 3.3. Программа мутант P1. double PowerMutant1(double x, int n) { double z=2; int i; if (n>0) { for (i=1;n>=i;i++) { z = z*x; } } else printf( "Ошибка ! Степень числа n должна быть больше 0.\n"); return z; } Пример 3.3.1. Программа мутант P1.Измененное начальное значение переменной i и границы цикла в мутанте P2 помечено светлым тоном:
// Метод вычисляет неотрицательную // степень n числа x static public double PowerMutant2( double x, int n) { double z=1; if (n>0) { for (int i=0;n-1>=i;i++) { z = z*x; } } else Console.WriteLine( "Ошибка ! Степень числа n должна быть больше 0"); return z; } Пример 3.4. Программа-мутант P2. double PowerMutant2(double x, int n) { double z=1; int i; if (n>0) { for (i=0;n-1>=i;i++) { z = z*x; } } else printf( "Ошибка ! Степень числа n должна быть больше 0.\n"); return z; } Пример 3.4.1. Программа-мутант P2.

Стохастические критерии (класс III)
Стохастическое тестирование применяется при тестировании сложных программных комплексов - когда набор детерминированных тестов (X,Y) имеет громадную мощность. В случаях, когда подобный набор невозможно разработать и исполнить на фазе тестирования, можно применить следующую методику.
- Разработать программы - имитаторы случайных последовательностей входных сигналов {x}.
- Вычислить независимым способом значения {y} для соответствующих входных сигналов {x} и получить тестовый набор (X,Y).
- Протестировать приложение на тестовом наборе (X,Y), используя два способа контроля результатов:
- Детерминированный контроль - проверка соответствия вычисленного значения yв{y} значению y, полученному в результате прогона теста на наборе {x} - случайной последовательности входных сигналов, сгенерированной имитатором.

- Стохастический контроль - проверка соответствия множества значений {yв}, полученного в результате прогона тестов на наборе входных значений {x}, заранее известному распределению результатов F(Y).
В этом случае множество Y неизвестно (его вычисление невозможно), но известен закон распределения данного множества.
- Детерминированный контроль - проверка соответствия вычисленного значения yв
- Cтатистические методы окончания тестирования - стохастические методы принятия решений о совпадении гипотез о распределении случайных величин. К ним принадлежат широко известные: метод Стьюдента (St), метод Хи-квадрат (?2) и т.п.
- Метод оценки скорости выявления ошибок - основан на модели скорости выявления ошибок [12], согласно которой тестирование прекращается, если оцененный интервал времени между текущей ошибкой и следующей слишком велик для фазы тестирования приложения.

Рис. 3.1. Зависимость скорости выявления ошибок от времени выявления
При формализации модели скорости выявления ошибок (рис. 3.1) использовались следующие обозначения:
N - исходное число ошибок в программном комплексе перед тестированием,
C - константа снижения скорости выявления ошибок за счет нахождения очередной ошибки,
t1,t2,…tn - кортеж возрастающих интервалов обнаружения последовательности из n ошибок,
T - время выявления n ошибок.
Если допустить, что за время T выявлено n ошибок, то справедливо соотношение (1), утверждающее, что произведение скорости выявления i ошибки и времени выявления i ошибки есть 1 по определению:
(1) (N-i+1)*C*ti = 1 В этом предположении справедливо соотношение (2) для n ошибок:
(2) N*C*t1+(N-1)*C*t2+…+(N-n+1)*C*tn=n N*C*(t1+t2+…+tn) - C*?(i-1)ti = n NCT - C*?(i-1)ti = n Если из (1) определить ti и просуммировать от 1 до n, то придем к соотношению (3) для времени T выявления n ошибок
(3) ?1/(N-i+1) = TC Если из (2) выразить C, приходим к соотношению (4):
(4) C = n/(NT - ?(i-1)ti) Наконец, подставляя C в (3), получаем окончательное соотношение (5), удобное для оценок:
(5) ?1/(N-i+1) = n/(N - 1/T*?(i-1)ti) Если оценить величину N приблизительно, используя известные методы оценки числа ошибок в программе [2], [13] или данные о плотности ошибок для проектов рассматриваемого класса из исторической базы данных проектов, и, кроме того, использовать текущие данные об интервалах между ошибками t1,t2…tn, полученные на фазе тестирования, то, подставляя эти данные в (5), можно получить оценку tn+1 -временного интервала необходимого для нахождения и исправления очередной ошибки (будущей ошибки).
Если tn+1>Td - допустимого времени тестирования проекта, то тестирование заканчиваем, в противном случае продолжаем поиск ошибок.
Наблюдая последовательность интервалов ошибок t1,t2…tn, и время, потраченное на выявление n ошибок T=?ti, можно прогнозировать интервал времени до следующей ошибки и уточнять в соответствии с (4) величину C.
Критерий Moranda очень практичен, так как опирается на информацию, традиционно собираемую в процессе тестирования.
Структурные критерии (класс I).
Структурные критерии используют модель программы в виде "белого ящика", что предполагает знание исходного текста программы или спецификации программы в виде потокового графа управления. Структурная информация понятна и доступна разработчикам подсистем и модулей приложения, поэтому данный класс критериев часто используется на этапах модульного и интеграционного тестирования (Unit testing, Integration testing).
Структурные критерии базируются на основных элементах УГП, операторах, ветвях и путях.
- Условие критерия тестирования команд (критерий С0) - набор тестов в совокупности должен обеспечить прохождение каждой команды не менее одного раза. Это слабый критерий, он, как правило, используется в больших программных системах, где другие критерии применить невозможно.
- Условие критерия тестирования ветвей (критерий С1) - набор тестов в совокупности должен обеспечить прохождение каждой ветви не менее одного раза. Это достаточно сильный и при этом экономичный критерий, поскольку множество ветвей в тестируемом приложении конечно и не так уж велико. Данный критерий часто используется в системах автоматизации тестирования.
- Условие критерия тестирования путей (критерий С2) - набор тестов в совокупности должен обеспечить прохождение каждого пути не менее 1 раз. Если программа содержит цикл (в особенности с неявно заданным числом итераций), то число итераций ограничивается константой (часто - 2, или числом классов выходных путей).
1 public void Method (ref int x) { 2 if (x>17) 3 x = 17-x; 4 if (x==-13) 5 x = 0; 6 } Пример 3.1. Пример простой программы, для тестирования по структурным критериям 1 void Method (int *x) { 2 if (*x>17) 3 *x = 17-*x; 4 if (*x==-13) 5 *x = 0; 6 } Пример 3.1.1. Пример простой программы, для тестирования по структурным критериямТестовый набор из одного теста, удовлетворяет критерию команд (C0):
(X,Y)={(xвх=30, xвых=0)} покрывает все операторы трассы 1-2-3-4-5-6
Тестовый набор из двух тестов, удовлетворяет критерию ветвей (C1):
(X,Y)={(30,0), (17,17)} добавляет 1 тест к множеству тестов для С0 и трассу 1-2-4-6. Трасса 1-2-3-4-5-6 проходит через все ветви достижимые в операторах if при условии true, а трасса 1-2-4-6 через все ветви, достижимые в операторах if при условии false.
Тестовый набор из четырех тестов, удовлетворяет критерию путей (C2):
(X,Y)={(30,0), (17,17), (-13,0), (21,-4)}
Набор условий для двух операторов if c метками 2 и 4 приведен в таблица 3.1
| (30,0) | (17,17) | (-13,0) | (21,-4) | |
| 2 if (x>17) | > | ![]() | ![]() | > |
| 4 if (x==-13) | = | ![]() | = | ![]() |
Например, если спецификация задает условие, что|x|

Структурные критерии не проверяют соответствие спецификации, если оно не отражено в структуре программы. Поэтому при успешном тестировании программы по критерию C2 мы можем не заметить ошибку, связанную с невыполнением некоторых условий спецификации требований.
Требования к идеальному критерию тестирования
Требования к идеальному критерию были выдвинуты в работе [11]:
- Критерий должен быть достаточным, т.е. показывать, когда некоторое конечное множество тестов достаточно для тестирования данной программы.
- Критерий должен быть полным, т.е. в случае ошибки должен существовать тест из множества тестов, удовлетворяющих критерию, который раскрывает ошибку.
- Критерий должен быть надежным, т.е. любые два множества тестов, удовлетворяющих ему, одновременно должны раскрывать или не раскрывать ошибки программы
- Критерий должен быть легко проверяемым, например вычисляемым на тестах
Поэтому мы стремимся к идеальному общему критерию через реальные частные.
Оценка Покрытия Программы и Проекта
Методика интегральной оценки тестированности
- Выбор критерия С и приемочной оценки тестированности программного проекта - L
- Построение дерева классов проекта и построение УГП для каждого модуля
- Модульное тестирование и оценка TV на модульном уровне
- Построение УГП, интегрирующего модули в единую иерархическую (классовую) модель проекта
- Выбор тестовых путей для проведения интеграционного или системного тестирования
- Генерация тестов, покрывающих тестовые пути шага 5
- Интегральная оценка тестированности проекта с учетом оценок тестированности модулей-компонентов
- Повторение шагов 5-7 до достижения заданного уровня тестированности L

Оценка Покрытия Программы и Проекта
Тестирование программы Р по некоторому критерию С означает покрытие множества компонентов программы P М = {m1...mk} по элементам или по связям
T = {t1...tn} - кортеж неизбыточных тестов ti.
Тест ti неизбыточен, если существует покрытый им компонент mi из M(P,C), не покрытый ни одним из предыдущих тестов t1...ti-1. Каждому ti соответствует неизбыточный путь pi - последовательность вершин от входа до выхода.
V(P,C) - сложность тестирования Р по критерию С - измеряется max числом неизбыточных тестов, покрывающих все элементы множества M(P,C)
DV(P,C,Т) - остаточная сложность тестирования Р по критерию С - измеряется max числом неизбыточных тестов, покрывающих элементы множества M(P,C), оставшиеся непокрытыми, после прогона набора тестов Т. Величина DV строго и монотонно убывает от V до 0.
TV(P,C,Т) = (V-DV)/V - оценка степени тестированности Р по критерию С.
Критерий окончания тестирования TV(P,C,Т)




Рис. 4.1. Метрика оттестированности приложения
Рассмотрим две модели программного обеспечения, используемые при оценке оттестированности.
Для оценки степени оттестированности часто используется УГП - управляющий граф программы. УГП многокомпонентного объекта G (Рис. 4.2, Пример 4.4), содержит внутри себя два компонента G1 и G2, УГП которых раскрыты.

Рис. 4.2. Плоская модель УГП компонента G
В результате УГП компонента G имеет такой вид, как если бы компоненты G1 и G2 в его структуре специально не выделялись, а УГП компонентов G1 и G2 были вставлены в УГП G. Для тестирования компонента G в соответствии с критерием путей потребуется прогнать тестовый набор, покрывающий следующий набор трасс графа G (Пример 4.1):
P1(G) = 1-2-3-4-5-6-7-10; P2(G) = 1-2-3-4-6-7-10; P3(G) = 1-2-11-16-18-14-15-7-10; P4(G) = 1-2-11-16-17-14-15-7-10; P5(G) = 1-2-11-16-12-13-14-15-7-10; P6(G) = 1-2-19-20-23-22-7-10; P7(G) = 1-2-19-20-21-22-7-10; Пример 4.1. Набор трасс, необходимых для покрытия плоской модели УГП компонента G

Рис. 4.3. Иерархическая модель УГП компонента G
УГП компонента G, представленный в виде иерархической модели, приведен на Рис. 4.3, Пример 4.5. В иерархическом УГП G входящие в его состав компоненты представлены ссылками на свои УГП G1 и G2 (Рис. 4.4, Пример 4.5)

Рис. 4.4. Иерархическая модель: УГП компонент G1 и G2
Для исчерпывающего тестирования иерархической модели компонента G в соответствии с критерием путей требуется прогнать следующий набор трасс (Пример 4.2):
P1(G) = 1-2-3-4-5-6-7-10; P2(G) = 1-2-3-4-6-7-10; P3(G) = 1-2-8-7-10; P4(G) = 1-2-9-7-10. Пример 4.2. Набор трасс, необходимых для покрытия иерархической модели УГП компонента GПриведенный набор трасс достаточен при условии, что компоненты G1 и G2 в свою очередь исчерпывающе протестированы. Чтобы обеспечить выполнение этого условия в соответствии с критерием путей, надо прогнать все трассы Пример 4.3.
P11(G1)=11-16-12-13-14-15; P21(G2)=19-20-21-22; P12(G1)=11-16-17-14-15; P22(G2)=11-16-18-17-14-15 P13(G1)=19-20-23-22. Пример 4.3. Набор трасс иерархической модели УГП, необходимых для покрытия УГП компонентов G1 и G2Оценка степени тестированности плоской модели определяется долей прогнанных трасс из набора необходимых для покрытия в соответствии с критерием С.
(1) TV(G,С) = (V-DV)/V =


где PTi(G) - тестовый путь (ti) в графе G плоской модели равен 1, если он протестирован (прогнан), или 0, если нет.
Например, если в УГП (Пример 4.1) тесты t6 и t8, которым соответствуют трассы P6 и P8, не прогнаны, то в соответствии с соотношением (1) для TV(G,С) степень тестированности будет оценена в 0.71.
Оценка тестированности иерархической модели определяется на основе учета оценок тестированности компонентов. Если трасса некоторого теста tj УГП G включает узлы, представляющие компоненты Gj1,..Gjm, оценка TV степени тестированности которых известна, то оценка тестированности PTi(G) при реализации этой трассы определяется не 1, а минимальной из оценок TV для компонентов.
Интегральная оценка определяется соотношением (2):
(2) TV(G,C) = (V-DV)/V = (



где PTi(G) - тестовый путь (ti) в графе G равен 1, если протестирован, или 0, если нет. В путь PTi графа G может входить j узлов модулей Gij со своей степенью тестированности TV(Gij,С) из которых мы берем min, что дает худшую оценку степени тестированности пути.
// Пример плоской модели проекта public void G() { int TerminalStatus=0, CommandStatus=0; bool IsPresent=true, CommandFound=true; 1 Init(); 2 switch (TerminalStatus) { case 11 : 11 AddCommand(); 16 switch (CommandStatus) { case 12 : 12 GetMessage(); 13 ClearQueue(); break; case 17 : 17 ClearQueue(); break; case 18 : 18 DumpQueue(); break; } 14 ProcessCommand(); 15 Commit(); break; case 3 : 3 AskTerminal(); 4 if (IsPresent) { 5 Connect(); } 6 RebuildQueue(); break; case 19 : 19 SearchValidCommand(); 20 if (CommandFound) { 21 AnalyzeCommand(); } else { 23 LogError(); } 22 MoveNextCommand(); break; } 7 LogResults(); 10 DisposeAll(); } Пример 4.4. Пример программы для плоской модели (Рис. 4.2) // Пример плоской модели проекта void G() { int TerminalStatus=0, CommandStatus=0; int IsPresent=1, CommandFound=1; 1 Init(); 2 switch (TerminalStatus) { case 11 : 11 AddCommand(); 16 switch (CommandStatus) { case 12 : 12 GetMessage(); 13 ClearQueue(); break; case 17 : 17 ClearQueue(); break; case 18 : 18 DumpQueue(); break; } 14 ProcessCommand(); 15 Commit(); break; case 3 : 3 AskTerminal(); 4 if (IsPresent) { 5 Connect(); } 6 RebuildQueue(); break; case 19 : 19 SearchValidCommand(); 20 if (CommandFound) { 21 AnalyzeCommand(); } else { 23 LogError(); } 22 MoveNextCommand(); break; } 7 LogResults(); 10 DisposeAll(); } Пример 4.4.1. Пример программы для плоской модели (Рис. 4.2) // Пример иерархической модели проекта public void G1() { int CommandStatus=0; AddCommand(); switch (CommandStatus) { case 12 : GetMessage(); ClearQueue(); break; case 17 : ClearQueue(); break; case 18 : DumpQueue(); break; } ProcessCommand(); Commit(); } public void G2() { bool CommandFound=true; SearchValidCommand(); if (CommandFound) { AnalyzeCommand(); } else { LogError(); } MoveNextCommand(); } public void G() { int TerminalStatus=0; bool IsPresent=true; 1 Init(); 2 switch (TerminalStatus) { case 11 : 8 G1(); break; case 3 : 3 AskTerminal(); 4 if (IsPresent) { 5 Connect(); } 6 RebuildQueue(); break; case 19 : // Пример иерархической модели проекта - продолжение 9 G2(); break; } 7 LogResults(); 10 DisposeAll(); } Пример 4.5.
Пример программы для иерархической модели (Рис. 4.3) // Пример иерархической модели проекта void G1() { int CommandStatus=0; AddCommand(); switch (CommandStatus) { case 12 : GetMessage(); ClearQueue(); break; case 17 : ClearQueue(); break; case 18 : DumpQueue(); break; } ProcessCommand(); Commit(); } void G2() { intl CommandFound=1; SearchValidCommand(); if (CommandFound) { AnalyzeCommand(); } else { LogError(); } MoveNextCommand(); } void G() { int TerminalStatus=0; int IsPresent=1; 1 Init(); 2 switch (TerminalStatus) { case 11 : 8 G1(); break; case 3 : 3 AskTerminal(); 4 if (IsPresent) { 5 Connect(); } 6 RebuildQueue(); break; case 19 : 9 G2(); break; } 7 LogResults(); 10 DisposeAll(); } Пример 4.5.1. Пример программы для иерархической модели (Рис. 4.3)
Пример модульного тестирования
Интеграционное тестирование
Интеграционное тестирование - это тестирование части системы, состоящей из двух и более модулей. Основная задача интеграционного тестирования - поиск дефектов, связанных с ошибками в реализации и интерпретации интерфейсного взаимодействия между модулями.
С технологической точки зрения интеграционное тестирование является количественным развитием модульного, поскольку так же, как и модульное тестирование, оперирует интерфейсами модулей и подсистем и требует создания тестового окружения, включая заглушки (Stub) на месте отсутствующих модулей. Основная разница между модульным и интеграционным тестированием состоит в целях, то есть в типах обнаруживаемых дефектов, которые, в свою очередь, определяют стратегию выбора входных данных и методов анализа. В частности, на уровне интеграционного тестирования часто применяются методы, связанные с покрытием интерфейсов, например, вызовов функций или методов, или анализ использования интерфейсных объектов, таких как глобальные ресурсы, средства коммуникаций, предоставляемых операционной системой.

Рис. 5.1. Пример структуры комплекса программ
На Рис. 5.1 приведена структура комплекса программ K, состоящего из оттестированных на этапе модульного тестирования модулей M1, M2, M11, M12, M21, M22. Задача, решаемая методом интеграционного тестирования, - тестирование межмодульных связей, реализующихся при исполнении программного обеспечения комплекса K. Интеграционное тестирование использует модель "белого ящика" на модульном уровне. Поскольку тестировщику текст программы известен с детальностью до вызова всех модулей, входящих в тестируемый комплекс, применение структурных критериев на данном этапе возможно и оправдано.
Интеграционное тестирование применяется на этапе сборки модульно оттестированных модулей в единый комплекс. Известны два метода сборки модулей:
- Монолитный, характеризующийся одновременным объединением всех модулей в тестируемый комплекс
- Инкрементальный, характеризующийся пошаговым (помодульным) наращиванием комплекса программ с пошаговым тестированием собираемого комплекса.
В инкрементальном методе выделяют две стратегии добавления модулей:- "Сверху вниз" и соответствующее ему восходящее тестирование.
- "Снизу вверх" и соответственно нисходящее тестирование.
Сравнение монолитного и интегрального подхода дает следующее:
- Монолитное тестирование требует больших трудозатрат, связанных с дополнительной разработкой драйверов и заглушек и со сложностью идентификации ошибок, проявляющихся в пространстве собранного кода.
- Пошаговое тестирование связано с меньшей трудоемкостью идентификации ошибок за счет постепенного наращивания объема тестируемого кода и соответственно локализации добавленной области тестируемого кода.
- Монолитное тестирование предоставляет большие возможности распараллеливания работ особенно на начальной фазе тестирования.
Например, порядок тестирования комплекса K (Рис. 5.1) при нисходящем тестировании может быть таким, как показано в примере 5.3, где тестовый набор, разработанный для модуля Mi, обозначен как XYi = (X, Y)i
1) K->XYK 2) M1->XY1 3) M11->XY11 4) M2->XY2 5) M22->XY22 6) M21->XY21 7) M12->XY12 Пример 5.3. Возможный порядок тестов при нисходящем тестированииНедостатки нисходящего тестирования:
- Проблема разработки достаточно "интеллектуальных" заглушек, т.е. заглушек, способных к использованию при моделировании различных режимов работы комплекса, необходимых для тестирования
- Сложность организации и разработки среды для реализации исполнения модулей в нужной последовательности
- Параллельная разработка модулей верхних и нижних уровней приводит к не всегда эффективной реализации модулей из-за подстройки (специализации) еще не тестированных модулей нижних уровней к уже оттестированным модулям верхних уровней
Например, порядок тестирования комплекса K (Рис. 5.1) при восходящем тестировании может быть следующим (пример. 5.4).
1) M11->XY11 2) M12->XY12 3) M1->XY1 4) M21->XY21 5) M2(M21, Stub(M22))->XY2 6) K(M1, M2(M21, Stub(M22)) ->XYK 7) M22->XY22 8) M2->XY2 9) K->XYK Пример 5.4. Возможный порядок тестов при восходящем тестированииНедостатки восходящего тестирования:
- Запаздывание проверки концептуальных особенностей тестируемого комплекса
- Необходимость в разработке и использовании драйверов
Модульное
Модульное тестирование - это тестирование программы на уровне отдельно взятых модулей, функций или классов. Цель модульного тестирования состоит в выявлении локализованных в модуле ошибок в реализации алгоритмов, а также в определении степени готовности системы к переходу на следующий уровень разработки и тестирования. Модульное тестирование проводится по принципу "белого ящика", то есть основывается на знании внутренней структуры программы, и часто включает те или иные методы анализа покрытия кода.
Модульное тестирование обычно подразумевает создание вокруг каждого модуля определенной среды, включающей заглушки для всех интерфейсов тестируемого модуля. Некоторые из них могут использоваться для подачи входных значений, другие для анализа результатов, присутствие третьих может быть продиктовано требованиями, накладываемыми компилятором и сборщиком.
На уровне модульного тестирования проще всего обнаружить дефекты, связанные с алгоритмическими ошибками и ошибками кодирования алгоритмов, типа работы с условиями и счетчиками циклов, а также с использованием локальных переменных и ресурсов. Ошибки, связанные с неверной трактовкой данных, некорректной реализацией интерфейсов, совместимостью, производительностью и т.п. обычно пропускаются на уровне модульного тестирования и выявляются на более поздних стадиях тестирования.
Именно эффективность обнаружения тех или иных типов дефектов должна определять стратегию модульного тестирования, то есть расстановку акцентов при определении набора входных значений. У организации, занимающейся разработкой программного обеспечения, как правило, имеется историческая база данных (Repository) разработок, хранящая конкретные сведения о разработке предыдущих проектов: о версиях и сборках кода (build) зафиксированных в процессе разработки продукта, о принятых решениях, допущенных просчетах, ошибках, успехах и т.п. Проведя анализ характеристик прежних проектов, подобных заказанному организации, можно предохранить новую разработку от старых ошибок, например, определив типы дефектов, поиск которых наиболее эффективен на различных этапах тестирования.
В данном случае анализируется этап модульного тестирования. Если анализ не дал нужной информации, например, в случае проектов, в которых соответствующие данные не собирались, то основным правилом становится поиск локальных дефектов, у которых код, ресурсы и информация, вовлеченные в дефект, характерны именно для данного модуля. В этом случае на модульном уровне ошибки, связанные, например, с неверным порядком или форматом параметров модуля, могут быть пропущены, поскольку они вовлекают информацию, затрагивающую другие модули (а именно, спецификацию интерфейса), в то время как ошибки в алгоритме обработки параметров довольно легко обнаруживаются.
Являясь по способу исполнения структурным тестированием или тестированием "белого ящика", модульное тестирование характеризуется степенью, в которой тесты выполняют или покрывают логику программы (исходный текст). Тесты, связанные со структурным тестированием, строятся по следующим принципам:
- На основе анализа потока управления. В этом случае элементы, которые должны быть покрыты при прохождении тестов, определяются на основе структурных критериев тестирования С0, С1,С2. К ним относятся вершины, дуги, пути управляющего графа программы (УГП), условия, комбинации условий и т. п.
- На основе анализа потока данных, когда элементы, которые должны быть покрыты, определяются на основе потока данных, т. е. информационного графа программы.
К популярным критериям относятся критерий покрытия функций программы, согласно которому каждая функция программы должна быть вызвана хотя бы один раз, и критерий покрытия вызовов, согласно которому каждый вызов каждой функции в программе должен быть осуществлен хотя бы один раз.
Критерий покрытия вызовов известен также как критерий покрытия пар вызовов (call pair coverage).
Тестирование на основе потока данных. Этот вид тестирования направлен на выявление ссылок на неинициализированные переменные и избыточные присваивания (аномалий потока данных). Как основа для стратегии тестирования поток данных впервые был описан в [14]. Предложенная там стратегия требовала тестирования всех взаимосвязей, включающих в себя ссылку (использование) и определение переменной, на которую указывает ссылка (т. е. требуется покрытие дуг информационного графа программы). Недостаток стратегии в том, что она не включает критерий С1, и не гарантирует покрытия решений.
Стратегия требуемых пар [15] также тестирует упомянутые взаимосвязи. Использование переменной в предикате дублируется в соответствии с числом выходов решения, и каждая из таких требуемых взаимосвязей должна быть протестирована. К популярным критериям принадлежит критерий СР, заключающийся в покрытии всех таких пар дуг v и w, что из дуги v достижима дуга w, поскольку именно на дуге может произойти потеря значения переменной, которая в дальнейшем уже не должна использоваться. Для "покрытия" еще одного популярного критерия Cdu достаточно тестировать пары (вершина, дуга), поскольку определение переменной происходит в вершине УГП, а ее использование - на дугах, исходящих из решений, или в вычислительных вершинах.
Методы проектирования тестовых путей для достижения заданной степени тестированности в структурном тестировании. Процесс построения набора тестов при структурном тестировании принято делить на три фазы:
- Конструирование УГП.
- Выбор тестовых путей.
- Генерация тестов, соответствующих тестовым путям.
На третьей фазе по известным путям тестирования осуществляется поиск подходящих тестов, реализующих прохождение этих путей.
Вторая фаза обеспечивает выбор тестовых путей. Выделяют три подхода к построению тестовых путей:
- Статические методы.
- Динамические методы.
- Методы реализуемых путей.
Динамические методы. Такие методы предполагают построение полной системы тестов, удовлетворяющих заданному критерию, путем одновременного решения задачи построения покрывающего множества путей и тестовых данных. При этом можно автоматически учитывать реализуемость или нереализуемость ранее рассмотренных путей или их частей. Основной идеей динамических методов является подсоединение к начальным реализуемым отрезкам путей дальнейших их частей так, чтобы: 1) не терять при этом реализуемости вновь полученных путей; 2) покрыть требуемые элементы структуры программы.
Методы реализуемых путей. Данная методика [16] заключается в выделении из множества путей подмножества всех реализуемых путей. После чего покрывающее множество путей строится из полученного подмножества реализуемых путей.
Достоинство статических методов состоит в сравнительно небольшом количестве необходимых ресурсов, как при использовании, так и при разработке. Однако их реализация может содержать непредсказуемый процент брака (нереализуемых путей). Кроме того, в этих системах переход от покрывающего множества путей к полной системе тестов пользователь должен осуществить вручную, а эта работа достаточно трудоемкая.Динамические методы требуют значительно больших ресурсов как при разработке, так и при эксплуатации, однако увеличение затрат происходит, в основном, за счет разработки и эксплуатации аппарата определения реализуемости пути (символический интерпретатор, решатель неравенств). Достоинство этих методов заключается в том, что их продукция имеет некоторый качественный уровень - реализуемость путей. Методы реализуемых путей дают самый лучший результат.
Особенности интеграционного тестирования для процедурного программирования
Процесс построения набора тестов при структурном тестировании определяется принципом, на котором основывается конструирование Графа Модели Программы (ГМП). От этого зависит множество тестовых путей и генерация тестов, соответствующих тестовым путям.
Первым подходом к разработке программного обеспечения является процедурное (модульное) программирование. Традиционное процедурное программирование предполагает написание исходного кода в императивном (повелительном) стиле, предписывающем определенную последовательность выполнения команд, а также описание программного проекта с помощью функциональной декомпозиции. Такие языки, как Pascal и C, являются императивными. В них порядок исходных строк кода определяет порядок передачи управления, включая последовательное исполнение, выбор условий и повторное исполнение участков программы. Каждый модуль имеет несколько точек входа (при строгом написании кода - одну) и несколько точек выхода (при строгом написании кода - одну). Сложные программные проекты имеют модульно-иерархическое построение [5], и тестирование модулей является начальным шагом процесса тестирования ПО. Построение графовой модели модуля является тривиальной задачей, а тестирование практически всегда проводится по критерию покрытия ветвей C1, т.е. каждая дуга и каждая вершина графа модуля должны содержаться, по крайней мере, в одном из путей тестирования.
Таким образом, M(P,C1) = E Nij , где Е - множество дуг, а Nij - входные вершины ГМП.
Сложность тестирования модуля по критерию С1 выражается уточненной формулой для оценки топологической сложности МакКейба [10]:
V(P,C1) = q + kin, где q - число бинарных выборов для условий ветвления, а kin - число входов графа.
Для интеграционного тестирования наиболее существенным является рассмотрение модели программы, построенной с использованием диаграмм потоков управления. Контролируются также связи через данные, подготавливаемые и используемые другими группами программ при взаимодействии с тестируемой группой. Каждая переменная межмодульного интерфейса проверяется на тождественность описаний во взаимодействующих модулях, а также на соответствие исходным программным спецификациям.
Состав и структура информационных связей реализованной группы модулей проверяются на соответствие спецификации требований этой группы. Все реализованные связи должны быть установлены, упорядочены и обобщены.
При сборке модулей в единый программный комплекс появляется два варианта построения графовой модели проекта:
- Плоская или иерархическая модель проекта (например, Рис. 4.2, Рис. 4.3).
- Граф вызовов.
| n - число узлов в графе; |
| e - число дуг в графе; |
| q - число бинарных выборов из условий ветвления в графе; |
| kin - число входов в граф; |
| kout - число выходов из графов; |
| kext - число точек входа, которые могут быть вызваны извне. |
Тогда сложность интеграционного тестирования всей программы P по критерию C1 может быть выражена формулой [17]:
V(P,C1) = ?V(Modi, C1) - kin +kext = e - n - kext + kout = q + kext, (


Рассмотрим вторую модель сборки модулей в процедурном программировании - граф вызовов. В этой модели в случае интеграционного тестирования учитываются только вызовы модулей в программе. Поэтому из множества M(Modi,C) тестируемых элементов можно исключить те элементы, которые не подвержены влиянию интеграции, т. е. узлы и дуги, не соединенные с вызовами модулей: M(Modi,C') = E'


V'(P,C1') = ? V'(Modi, C1') - kin +kext Так, для программы, ГМП которой приведена на Рис. 4.2, для получения графа вызовов из иерархической модели проекта должны быть исключены все дуги, кроме:
- Дуги 1-2, содержащей входной узел 1 графа G.
- Дуг 2-8, 8-7, 7-10, содержащих вызов модуля G1.
- Дуг 2-9, 9-7, 7-10, содержащих вызов модуля G2.
V'(G,C1') = q + Kext =1+1=2. V'(Modi,C') также называется в литературе сложностью модульного дизайна (complexity of module design) [19].

Рис. 5.2. Граф вызовов модулей
Сумма сложностей модульного дизайна для всех модулей по критерию С1 или сумма их аналогов для других критериев тестирования, исключая значения модулей самого нижнего уровня, дает сложность интеграционного тестирования для процедурного программирования [20].

Пример модульного тестирования
Предлагается протестировать класс TCommand, который реализует команду для склада. Этот класс содержит единственный метод TCommand.GetFullName(), спецификация которого описана (Практикум, Приложение 2 HLD) следующим образом:
| ... | |
| Операция GetFullName() возвращает полное имя команды, соответствующее ее допустимому коду, указанному в поле NameCommand. В противном случает возвращается сообщение "ОШИБКА : Неверный код команды". Операция может быть применена в любой момент. | |
| ... |
Разработаем спецификацию тестового случая для тестирования метода GetFullName на основе приведенной спецификации класса (Табл. 5.1):
| Название класса: TСommand | Название тестового случая: TСommandTest1 |
| Описание тестового случая: Тест проверяет правильность работы метода GetFullName - получения полного названия команды на основе кода команды. В тесте подаются следующие значения кодов команд (входные значения): -1, 1, 2, 4, 6, 20, (причем -1 - запрещенное значение). | |
| Начальные условия: Нет. | |
| Ожидаемый результат: Перечисленным входным значениям должны соответствовать следующие выходные: Коду команды -1 должно соответствовать сообщение "ОШИБКА: Неверный код команды" Коду команды 1 должно соответствовать полное название команды "ПОЛУЧИТЬ ИЗ ВХОДНОЙ ЯЧЕЙКИ" Коду команды 2 должно соответствовать полное название команды "ОТПРАВИТЬ ИЗ ЯЧЕЙКИ В ВЫХОДНУЮ ЯЧЕЙКУ" Коду команды 4 должно соответствовать полное название команды "ПОЛОЖИТЬ В РЕЗЕРВ" Коду команды 6 должно соответствовать полное название команды "ПРОИЗВЕСТИ ЗАНУЛЕНИЕ" Коду команды 20 должно соответствовать полное название команды "ЗАВЕРШЕНИЕ КОМАНД ВЫДАЧИ" | |
Пары значений (X, Yв) при исполнении теста заносятся в log- файл для последующей проверки на соответствие спецификации.
После завершения теста следует просмотреть журнал теста, чтобы сравнить полученные результаты с ожидаемыми, заданными в спецификации тестового случая TСommandTest1 (Пример 5.2).
class TCommandTester:Tester // Тестовый драйвер { ... TCommand OUT; public TCommandTester() { OUT=new TCommand(); Run(); } private void Run() { TCommandTest1(); } private void TCommandTest1() { int[] commands = {-1, 1, 2, 4, 6, 20}; for(int i=0;i<=5;i++) { OUT.NameCommand=commands[i]; LogMessage(commands[i].ToString()+ " : "+OUT.GetFullName()); } } ... } Пример 5.1. Тестовый драйвер TCommandQueueTester::TCommandQueueTester() { TB = new TTerminalBearing(); S = new TStore(); CommandQueue=new TCommandQueue(S,TB); S->CommandQueue=CommandQueue; } Пример 5.1.1. Тестовый драйвер на С++ -1 : ОШИБКА : Неверный код команды 1 : ПОЛУЧИТЬ ИЗ ВХОДНОЙ ЯЧЕЙКИ 2 : ОТПРАВИТЬ ИЗ ЯЧЕЙКИ В ВЫХОДНУЮ ЯЧЕЙКУ 4 : ПОЛОЖИТЬ В РЕЗЕРВ 6 : ПРОИЗВЕСТИ ЗАНУЛЕНИЕ 20 : ЗАВЕРШЕНИЕ КОМАНД ВЫДАЧИ Пример 5.2. Спецификация классов тестовых случаев
Особенности интеграционного тестирования
Особенности интеграционного тестирования для объектно-ориентированного программирования
Программный проект, написанный в соответствии с объектно-ориентированным подходом, будет иметь ГМП, существенно отличающийся от ГМП традиционной "процедурной" программы. Сама разработка проекта строится по другому принципу - от определения классов, используемых в программе, построения дерева классов к реализации кода проекта. При правильном использовании классов, точно отражающих прикладную область приложения, этот метод дает более короткие, понятные и легко контролируемые программы.
Объектно-ориентированное программное обеспечение является событийно управляемым. Передача управления внутри программы осуществляется не только путем явного указания последовательности обращений одних функций программы к другим, но и путем генерации сообщений различным объектам, разбора сообщений соответствующим обработчиком и передача их объектам, для которых данные сообщения предназначены. Рассмотренная ГМП в данном случае становится неприменимой. Эта модель, как минимум, требует адаптации к требованиям, вводимым объектно-ориентированным подходом к написанию программного обеспечения. При этом происходит переход от модели, описывающей структуру программы, к модели, описывающей поведение программы, что для тестирования можно классифицировать как положительное свойство данного перехода. Отрицательным аспектом совершаемого перехода для применения рассмотренных ранее моделей является потеря заданных в явном виде связей между модулями программы.
Перед тем как приступить к описанию графовой модели объектно-ориентированной программы, остановимся отдельно на одном существенном аспекте разработки программного обеспечения на языке объектно-ориентированного программирования (ООП), например, C++ или С#. Разработка программного обеспечения высокого качества для MS Windows или любой другой операционной системы, использующей стандарт "look and feel", с применением только вновь созданных классов практически невозможна. Программист должен будет затратить массу времени на решение стандартных задач по созданию пользовательского интерфейса.
Чтобы избежать работы над давно решенными вопросами, во всех современных компиляторах предусмотрены специальные библиотеки классов. Такие библиотеки включают в себя практически весь программный интерфейс операционной системы и позволяют задействовать при программировании средства более высокого уровня, чем просто вызовы функций. Базовые конструкции и классы могут быть переиспользованы при разработке нового программного проекта. За счет этого значительно сокращается время разработки приложений. В качестве примера подобной системы можно привести библиотеку Microsoft Foundation Class для компилятора MS Visual C++ [21]".
Работа по тестированию приложения не должна включать в себя проверку работоспособности элементов библиотек, ставших фактически промышленным стандартом для разработки программного обеспечения, а только проверку кода, написанного непосредственно разработчиком программного проекта. Тестирование объектно-ориентированной программы должно включать те же уровни, что и тестирование процедурной программы - модульное, интеграционное и системное. Внутри класса отдельно взятые методы имеют императивный характер исполнения. Все языки ООП возвращают контроль вызывающему объекту, когда сообщение обработано. Поэтому каждый метод (функция - член класса) должен пройти традиционное модульное тестирование по выбранному критерию C (как правило, С1). В соответствии с введенными выше обозначениями, назовем метод Modi, а сложность тестирования - V(Modi,C). Все результаты, полученные в лекции 5 для тестирования модулей, безусловно, подходят для тестирования методов классов. Каждый класс должен быть рассмотрен и как субъект интеграционного тестирования. Интеграция для всех методов класса проводится с использованием инкрементальной стратегии снизу вверх. При этом мы можем переиспользовать тесты для классов-родителей тестируемого класса [22], что следует из принципа наследования - от базовых классов, не имеющих родителей, к самым верхним уровням классов.
Графовая модель класса, как и объектно-ориентированной программы, на интеграционном уровне в качестве узлов использует методы.
Дуги данной ГМП ( вызовы методов) могут быть образованы двумя способами:
- Прямым вызовом одного метода из кода другого, в случае, если вызываемый метод виден (не закрыт для доступа средствами языка программирования) из класса, содержащего вызывающий метод, присвоим такой конструкции название Р-путь (P-path, Procedure path, процедурный путь).
- Обработкой сообщения, когда явного вызова метода нет, но в результате работы "вызывающего" метода порождается сообщение, которое должно быть обработано "вызываемым" методом.
Пример ММ-путей приведен в примере 5.4. Данная конструкция отражает событийно управляемую природу объектно-ориентированного программирования и может быть взята в качестве основы для построения графовой модели класса или объектно-ориентированной программы в целом. В примере 5.4 можно выделить четыре ММ-пути (1-4) и один P-путь (5):
- msg a метод 3
msg 3
метод 4
msg d
- msg bметод 1
msg 1
метод 4
msg d
- msg bметод 1
msg 2
метод 5
- msg cметод 2

- callметод 5

Введем следующие обозначения:
Kmsg- число методов класса, обрабатывающих различные сообщения;
Kem- число методов класса, которые не закрыты от прямого вызова из других классов программы.

Рис. 6.1. Пример MM-путей и P-путей в графовой модели класса
Если рассматривать класс как программу P, то можно выделить следующие отличия от программы, построенной по процедурному принципу:
- Значение Kext (число точек входа, которые могут быть вызваны извне) определяется как сумма методов - обработчиков сообщений Kmsg (например, в MS Visual C++ обозначаются зарезервированным словом afx_msg и используются для работы с картой сообщений класса) и тех методов, которые могут быть вызваны из других классов программы Kem.
Это определяется самим разработчиком путем разграничения доступа к методам класса (с помощью ключевых слов разграничения доступа public, private, protect) при написании методов, а также назначении дружественных (friend) функций и дружественных классов. Таким образом, Kext = Kmsg + Kem, и имеет новый по сравнению с процедурным программированием физический смысл. - Принцип соединения узлов в ГМП, отражающий два возможных типа вызовов методов класса (через ММ-пути и Р-пути), что приводит к новому наполнению для множества М требуемых элементов.
- Методы (модули) непрозрачны для внешних объектов, что влечет за собой неприменимость механизма упрощения графа модуля, используемого для получения графа вызовов в процедурном программировании.
В ходе интеграционного тестирования должны быть проверены все возможные внешние вызовы методов класса, как непосредственные обращения, так и вызовы, инициированные получением сообщений
Значение числа ММ-путей зависит от схемы обработки сообщений данным классом, что должно быть определено в спецификации класса. Например, для класса, изображенного в примере 5.4, сложность интеграционного тестирования V(Cls,C)=5 (множество неизбыточных тестов Т для класса составляют 4 ММ-пути плюс внешний вызов метода 5, т. е. Р-путь).
Данные - члены класса (данные, описанные в самом классе, и унаследованные от классов-родителей видимые извне данные) рассматриваются как "глобальные переменные", они должны быть протестированы отдельно на основе принципов тестирования потоков данных.
Когда класс программы P протестирован, объект данного класса может быть включен в общий граф G программного проекта, содержащий все ММ-пути и все вызовы методов классов и процедур, возможные в программе рис. 5.2:
Программа P, содержащая n классов, имеет сложность интеграционного тестирования классов
V(P, C) =?V(Clsi, C)
Формальным представлением описанного выше подхода к тестированию программного проекта служит классовая модель программного проекта, состоящая из дерева классов проекта рис. 6.3 и модели каждого класса, входящего в программный проект рис. 6.4.

Рис. 6.2. Пример включения объекта в модель программного проекта, построенного с использованием MM-путей и P-путей
Таким образом и определяется классовая модель проекта для тестирования объектно-ориентированной программы. Как будет показано в дальнейшем, она поддерживает итерационный инкрементальный процесс разработки программного обеспечения.

Рис. 6.3. Дерево классов проекта

Рис. 6.4. Модель класса, входящего в программный проект
Методика проведения тестирования программы, представленной в виде классовой модели программного проекта, включает в себя несколько этапов, соответствующих уровням тестирования рис. 6.5:
- На первом уровне проводится тестирование методов каждого класса программы, что соответствует этапу модульного тестирования.
- На втором уровне тестируются методы класса, которые образуют контекст интеграционного тестирования каждого класса.
- На третьем уровне протестированный класс включается в общий контекст (дерево классов) программного проекта. Здесь становится возможным отслеживать реакцию программы на внешние события
Для третьего уровня важным оказывается понятие атомарной системной функции (АСФ) [23]. АСФ - это множество, состоящее из внешнего события на входе системы, реакции системы на это событие в виде одного или более ММ-путей и внешнего события на выходе системы. В общем случае внешнее выходное событие может быть нулевым, т. е. неаккуратно написанное программное обеспечение может не обеспечивать внешней реакции на действия пользователя. АСФ, состоящая из входного внешнего события, одного ММ-пути и выходного внешнего события, может быть взята в качестве модели для нити (thread).
Тестирование подобной АСФ в рамках классовой модели ГМП реализуется довольно сложно, так как хотя динамическое взаимодействие нитей (потоков) в процессе исполнения естественно фиксируется в log-файлах, запоминающих результаты трассировки исполнения программ, оно же достаточно сложно отображается на классовой ГМП. Причина в том, что классовая модель ориентирована на отображение статических характеристик проекта, а в данном случае требуется отображение поведенческих характеристик. Как правило, тестирование взаимодействия нитей в ходе исполнения программного комплекса выносится на уровень системного тестирования и использует другие более приспособленные для описания поведения модели. Например, описание поведения программного комплекса средствами языков спецификаций MSC, SDL, UML.
Явный учет границ между интеграционным и системным уровнями тестирования дает преимущество при планировании работ на фазе тестирования, а возможность сочетать различные методы и критерии тестирования в ходе работы над программным проектом дает наилучшие результаты [24].
Объектно-ориентированный подход, ставший в настоящее время неявным стандартом разработки программных комплексов, позволяет широко использовать иерархическую модель программного проекта, приведенная на рис. 6.5 схема иллюстрирует способ применения. Каждый класс рассматривается как объект модульного и интеграционного тестирования. Сначала каждый метод класса тестируется как модуль по выбранному критерию C. Затем класс становится объектом интеграционного тестирования. Далее осуществляется интеграция всех методов всех классов в единую структуру - классовую модель проекта, где в общую ГМП протестированные модули входят в виде узлов (интерфейсов вызова) без учета их внутренней структуры, а их детальные описания образуют контекст всего программного проекта.

Рис. 6.5. Уровни тестирования классовой модели программного проекта
Сама технология объектно-ориентированного программирования (одним из определяющих принципов которой является инкапсуляция с возможностью ограничения доступа к данным и методам - членам класса) позволяет применить подобную трактовку вхождения модулей в общую ГМП.При этом тесты для отдельно рассмотренных классов переиспользуются, входя в общий набор тестов для программы P.
Пример интеграционного тестирования
Продемонстрируем тестирование взаимодействий на примере взаимодействия класса TCommandQueue и класса TСommand, а также, как и при модульном теcтировании, разработаем спецификацию тестового случая таблица 4.2:
| Названия взаимодействующих классов:TСommandQueue, TCommand | Название теста:TCommandQueueTest1 |
| Описание теста:тест проверяет возможность создания объекта типа TCommand и добавления его в очередь при вызове метода AddCommand | |
| Начальные условия:очередь команд пуста | |
| Ожидаемый результат:в очередь будет добавлена одна команда | |
Класс содержит:
- конструктор, в котором создаются объекты классов TStore, TTerminalBearing и объект типа TcommandQueue
- Методы, реализующие тесты. Каждый тест реализован в отдельном методе.
- Метод Run, в котором вызываются методы тестов.
- Метод dump, который сохраняет в Log-файле теста информацию обо всех командах, находящихся в очереди в формате - Номер позиции в очереди: полное название команды
- Точку входа в программу - метод Main, в котором происходит создание экземпляра класса TСommandQueueTester.
private void TCommandQueueTest1() { LogMessage("///// TCommandQueue Test1 /////"); LogMessage("Проверяем, создается ли объект типа TCommand"); // В очереди нет команд dump(); // Добавляем команду // параметр = -1 означает, что команда // должна быть добавлена в конец очереди CommandQueue.AddCommand(TCommand.GetR,0,0,0, new TBearingParam(),new TAxleParam(),-1); LogMessage("Command added"); // В очереди одна команда dump(); } Пример 6.2. Тест void TCommandQueueTester::TCommandQueueTest1() { LogMessage("///// TCommandQueue Test1 /////"); LogMessage("Проверяем, создается ли объект типа TCommand"); // В очереди нет команд dump(); // Добавляем команду // параметр = -1 означает, что команда // должна быть добавлена в конец очереди CommandQueue.AddCommand(GetR,0,0,0, new TBearingParam(), new TAxleParam(),-1); LogMessage("Command added"); // В очереди одна команда dump(); } Пример 6.2.1. Тест (C++)В класс включены еще два разработанных теста.
После завершения теста следует просмотреть текстовый журнал теста, чтобы сравнить полученные результаты с ожидаемыми результатами, заданными в спецификации тестового случая TCommandQueueTest1 пример 6.3.
///// TCommandQueue Test1 ///// Проверяем, создается ли объект типа TCommand 0 commands in command queue Command added 1 commands in command queue 0: ПОЛУЧИТЬ ИЗ ВХОДНОЙ ЯЧЕЙКИ Пример 6.3. Спецификация результатов теста

Системное тестирование
Комбинирование уровней тестирования
В каждом конкретном проекте должны быть определены задачи, ресурсы и технологии для каждого уровня тестирования таким образом, чтобы каждый из типов дефектов, ожидаемых в системе, был «адресован», то есть в общем наборе тестов должны иметься тесты, направленные на выявление дефектов подобного типа. Табл. 4.3 суммирует характеристики свойств модульного, интеграционного и системного уровней тестирования. Задача, которая стоит перед тестировщиками и менеджерами, заключается в оптимальном распределении ресурсов между всеми тремя типами тестирования. Например, перенесение усилий на поиск фиксированного типа дефектов из области системного в область модульного тестирования может существенно снизить сложность и стоимость всего процесса тестирования.
| Локальные дефекты, такие как опечатки в реализации алгоритма, неверные операции, логические и математические выражения, циклы, ошибки в использовании локальных ресурсов, рекурсия и т.п. | Интерфейсные дефекты, такие как неверная трактовка параметров и их формат, неверное использование системных ресурсов и средств коммуникации, и т.п. | Отсутствующая или некорректная функциональность, неудобство использования, непредусмотренные данные и их комбинации, непредусмотренные или не поддержанные сценарии работы, ошибки совместимости, ошибки пользовательской документации, ошибки переносимости продукта на различные платформы, проблемы производительности, инсталляции и т.п. |
| Да | Да | Нет (*) |
| Низкая | Низкая до умеренной | Умеренная до высокой или неприемлемой |
| Низкая | Низкая | Высокая |

Пример регрессионного тестирования
Получив отчет об ошибке, программист анализирует исходный код, находит ошибку, исправляет ее и модульно или интеграционно тестирует результат.
В свою очередь тестировщик, проверяя внесенные программистом изменения, должен:
- Проверить и утвердить исправление ошибки. Для этого необходимо выполнить указанный в отчете тест, с помощью которого была найдена ошибка.
- Попробовать воспроизвести ошибку каким-нибудь другим способом.
- Протестировать последствия исправлений. Возможно, что внесенные исправления привнесли ошибку (наведенную ошибку) в код, который до этого исправно работал.
// Тест проверяет, создается ли объект // типа TCommand и добавляется ли он // в конец очереди. private void TCommandQueueTest1() // Тест проверяет добавление команд // в очередь на указанную позицию. // Также проверяется правильность // удаления команд из очереди. private void TCommandQueueTest2() Пример 7.4. Набор тестов класса TСommandQueue// Тест проверяет, создается ли объект // типа TCommand и добавляется ли он // в конец очереди. void TCommandQueueTest1() // Тест проверяет добавление команд // в очередь на указанную позицию. // Также проверяется правильность // удаления команд из очереди. void TCommandQueueTest2() Пример 7.4.1. Набор тестов класса TСommandQueue (C++)При этом первый тест выполняется успешно, а второй нет, т.е. команда добавляется в конец очереди команд успешно, а на указанную позицию - нет. Разработчик анализирует код, который реализует тестируемую функциональность:
...
if ((Position<-1)&& (Position<=this.Items.Count)) { this.Items.Insert(Position, Command); } else { if (Position==-1) { this.Items.Add(Command); } } Пример 7.5. Фрагмент кода с зафиксированным при тестировании дефектомif ((Position <-1)&&(Position<=this.Items.Count)) { this.Items.Insert(Position, Command); } else { if (Position==-1) { this.Items.Add(Command); } } Пример 7.5.1. Фрагмент кода с зафиксированным при тестировании дефектомАнализ показывает, что ошибка заключается в использовании неверного знака сравнения в первой строке фрагмента (помечено светлым тоном).
Далее программист исправляет ошибку, например следующим образом:
... if ((Position>=-1)&& (Position<=this.Items.Count)) { this.Items.Insert(Position, Command); } else { if (Position==-1) { this.Items.Add(Command); } } ... Пример 7.6. Исправленный фрагмент кодаif ((Position>=-1)&& (Position<=this.Items.Count)) { this.Items.Insert(Position, Command); } else { if (Position==-1) { this.Items.Add(Command); } } Пример 7.6.1. Исправленный фрагмент кодаДля проверки скорректированного кода хочется пропустить только тест TCommandQueueTest2. Можно убедиться, что тест TCommandQueueTest2 будет выполняться успешно. Однако одной этой проверки недостаточно. Если мы повторим пропуск двух тестов, то при запуске первого теста, TCommandQueueTest1, будет обнаружен новый дефект. Повторный анализ кода показывает, что ветка else не выполняется. Таким образом, исправление в одном месте привело к ошибке в другом, что демонстрирует необходимость проведения полного перетестирования. Однако повторное перетестирование требует значительных усилий и времени. Возникает задача – отобрать сокращенный набор тестов из исходного набора (может быть, пополнив его рядом дополнительных - вновь разработанных - тестов), которого, тем не менее, будет достаточно для исчерпывающей проверки функциональности в соответствии с выбранным критерием. Организация повторного тестирования в условиях сокращения ресурсов, необходимых для обеспечения заданного уровня качества продукта, обеспечивается регрессионным тестированием.
Пример системного тестирования приложения «Поступление подшипника на склад»
В спецификации тестового случая задано состояние окружения (входные данные) и ожидаемая последовательность событий в системе (ожидаемый результат). После прогона тестового случая мы получаем реальную последовательность событий в системе (пример 7.1,пример 7.3) при заданном состоянии окружения. Сравнивая фактический результат с ожидаемым, можно сделать вывод о том, прошла или не прошла тестируемая система испытание на заданном тестовом случае. В качестве ожидаемого результата будем использовать спецификацию тестового случая, поскольку она определяет, как, для заданного состояния окружения, система должна функционировать.

Рис. 4-15. Краткое описание тестируемой системы 'Поступление подшипника на склад'
Спецификация тестового случая №1:
Состояние окружения (входные данные - X ):
Статус склада - 32. Пришел подшипник.
Статус обмена с терминалом подшипника (0 - есть подшипник) и его параметры - "Статус=0 Диаметр=12".
Статус обмена с терминалом оси (1 - нет оси) и ее параметры - "Статус=1 Диаметр=12".
"Статус=1 Диаметр=12".
Статус команды - 0. Команда успешно принята.
Сообщение от склада - 1. Команда успешно выполнена.
Ожидаемая последовательность событий (выходные данные – Y):
Система запрашивает статус склада (вызов функции GetStoreStat) и получает 32
Система запрашивает параметры подшипника (вызов функции GetRollerPar) и получает Статус = 0 Диаметр=12
Система запрашивает параметры оси (вызов функции GetAxlePar) и получает Статус = 1 Диаметр=0
Система добавляет в очередь команд склада на последнее место команду SendR (получить из приемника в ячейку) (вызов функции SendStoreCom) и получает сообщение о том, что команда успешно принята – статус = 0
Система запрашивает склад о результатах выполнения команды (вызов функции GetStoreMessage) и получает сообщение о том, что команда успешно выполнена - статус = 1
Выходные данные (результаты выполнения Yв) – зафиксированы в журнале теста (пример 7.1)
ВЫЗОВ: GetStoreStat РЕЗУЛЬТАТ: 32 ВЫЗОВ: GetRollerPar РЕЗУЛЬТАТ: Статус = 0 Диаметр = 12 ВЫЗОВ: GetAxlePar РЕЗУЛЬТАТ: Статус = 1 Диаметр = 0 ВЫЗОВ: SendStoreCom РЕЗУЛЬТАТ: 0 ВЫЗОВ: GetStoreMessage РЕЗУЛЬТАТ: 1 Пример 7.1.
Журнал тестаПриведенный на пример 7.2 тест был разработан в соответствии со спецификацией тестового случая №1. Детальная спецификация приведена в FS (Практикум, Приложение 1), результаты прогона показаны на пример 7.3.
class Test1:Test { override public void start() { // Задаем состояние окружения // (входные данные) StoreStat="32"; //Поступил подшипник RollerPar="0 NewUser Depot1 123456 1 12 1 1"; // статус обмена с терминалом подшипника // (0 - есть подшипник) и его параметры AxlePar="1 NewUser Depot1 123456 1 0 12 12"; // статус обмена с терминалом оси // (1 - нет оси) и ее параметры CommandStatus="0"; // команда успешно принята StoreMessage="1"; // успешно выполнена // Получаем информацию о функционировании // системы wait("GetStoreStat"); //опрос статуса склада wait("GetRollerPar"); // Получение информации о подшипнике // с терминала подшипника wait("GetAxlePar"); // Получение информации об оси // с терминала оси wait("SendStoreCom"); // добавление в очередь команд склада // на первое место команды GetR // (получить из приемника в ячейку) wait("GetStoreMessage"); // Получение сообщения от склада о // результатах выполнения команды // В результате первый подшипник // должен быть принят } } Пример 7.2. Тест для системного тестированияclass Test1 : public Test { public: void start() { // Задаем состояние окружения // (входные данные) // Поступил подшипник strcpy(StoreStat,"32"); // статус обмена с терминалом подшипника // (0 - есть подшипник) и его параметры strcpy(RollerPar, "0 NewUser Depot1 123456 1 12 1 1"); // статус обмена с терминалом оси // (1 - нет оси) и ее параметры strcpy(AxlePar, "1 NewUser Depot1 123456 1 0 12 12"); strcpy(CommandStatus,"0"); //команда успешно принята strcpy(StoreMessage,"1"); //успешно выполнена // Получаем информацию о // функционировании системы wait("GetStoreStat"); //опрос статуса склада wait("GetRollerPar"); // Получение информации о подшипнике // с терминала подшипника wait("GetAxlePar"); // Получение информации об оси // с терминала оси wait("SendStoreCom"); // добавление в очередь команд склада // на первое место команды GetR // (получить из приемника в ячейку) wait("GetStoreMessage"); // Получение сообщения от склада о // результатах выполнения команды // В результате первый подшипник // должен быть принят } } Пример 7.2.1.Тест для системного тестирования (C++)После завершения теста следует просмотреть текстовый журнал теста, чтобы выяснить, какая последовательность событий в системе была реально зафиксирована (выходные данные) и сравнить их с ожидаемыми результатами, заданными в спецификации тестового случая1. Пример журнала теста (пример 7.1):
Test started CALL:GetStoreStat 0 RETURN:32 CALL:GetRollerPar RETURN:0 NewUser Depot1 123456 1 12 1 1 CALL:GetAxlePar RETURN:1 NewUser Depot1 123456 1 0 12 12 CALL:SendStoreCom 1 0 0 1 0 0 0 RETURN:0 CALL:GetStoreMessage RETURN:1 Пример 7.3. Тестовый журнал для случая прогона системного теста
Регрессионное тестирование
Регрессионное тестирование- цикл тестирования, который производится при внесении изменений на фазе системного тестирования или сопровождения продукта. Главная проблема регрессионного тестирования- выбор между полным и частичным перетестированием и пополнением тестовых наборов. При частичном перетестировании контролируются только те части проекта, которые связаны с измененными компонентами. На ГМП это пути, содержащие измененные узлы, и, как правило, это методы и классы, лежащие выше модифицированных по уровню, но содержащие их в своем контексте
Пропуск огромного объема тестов, характерного для этапа системного тестирования, удается осуществить без потери качественных показателей продукта только с помощью регрессионного подхода. В данном пособии регрессионному тестированию посвящен раздел 6.
Системное тестирование
Системное тестирование качественно отличается от интеграционного и модульного уровней. Системное тестирование рассматривает тестируемую систему в целом и оперирует на уровне пользовательских интерфейсов, в отличие от последних фаз интеграционного тестирования, которое оперирует на уровне интерфейсов модулей. Различны и цели этих уровней тестирования. На уровне системы часто сложно и малоэффективно анализировать прохождение тестовых траекторий внутри программы или отслеживать правильность работы конкретных функций. Основная задача системного тестирования - в выявлении дефектов, связанных с работой системы в целом, таких как неверное использование ресурсов системы, непредусмотренные комбинации данных пользовательского уровня, несовместимость с окружением, непредусмотренные сценарии использования, отсутствующая или неверная функциональность, неудобство в применении и тому подобное.
Системное тестирование производится над проектом в целом с помощью метода «черного ящика». Структура программы не имеет никакого значения, для проверки доступны только входы и выходы, видимые пользователю. Тестированию подлежат коды и пользовательская документация.
Категории тестов системного тестирования:
- Полнота решения функциональных задач.
- Стрессовое тестирование - на предельных объемах нагрузки входного потока.
- Корректность использования ресурсов (утечка памяти, возврат ресурсов).
- Оценка производительности.
- Эффективность защиты от искажения данных и некорректных действий.
- Проверка инсталляции и конфигурации на разных платформах.
- Корректность документации
Автоматизация тестирования
Автоматизация тестирования
Использование различных подходов к тестированию определяется их эффективностью применительно к условиям, определяемым промышленным проектом. В реальных случаях работа группы тестирования планируется так, чтобы разработка тестов начиналась с момента согласования требований к программному продукту (выпуск Requirement Book, содержащей высокоуровневые требования к продукту) и продолжалась параллельно с разработкой дизайна и кода продукта. В результате, к началу системного тестирования создаются тестовые наборы, содержащие тысячи тестов. Большой набор тестов обеспечивает всестороннюю проверку функциональности продукта и гарантирует качество продукта, но пропуск такого количества тестов на этапе системного тестирования представляет проблему. Ее решение лежит в области автоматизации тестирования, т.е. в автоматизации разработки.
Структура программы P теста Загрузка теста (X,Y*) Запуск тестируемого модуля Cравнение полученных результатов Y c эталонными Y* Структура тестируемого комплекса ModF <- МоdF1 МоdF2 МоdF3 <- МоdF31 МоdF32 Структура тестирующего модуля Mod TestModF: Mod TestМодF1 Mоd TestМодF2 Mоd TestМодF3 P TestМодF Mоd TestModF1: P TestМодF1 Mоd TestModF2: P TestМодF2 Mоd TestModF3: Mod TestМодF31 Mоd TestМодF32 P TestМодF3 В этом примере приведены структура теста, структура тестируемого комплекса и структура тестирующего модуля. Особенностью структуры каждого из тестирующих модулей Mi является запуск тестирующей программы Pi после того как каждый из модулей Mij, входящих в контекст модуля Mi, оттестирован. В этом случае запуск тестирующего модуля обеспечивает рекурсивный спуск к программам тестирования модулей нижнего уровня, а затем исполняет тестирование вышележащих уровней в условиях оттестированности нижележащих. Тестовые наборы подобной структуры ориентированы на автоматическое управление пропуском тестового набора в тестовом цикле. Важным преимуществом подобной организации является возможность регулирования нижнего уровня, до которого следует доходить в цикле тестирования.
В этом случае контекст редуцированных в конкретном тестовом цикле модулей помечается как базовый, не подлежащий тестированию.Например, если контекст модуля ModF3: (ModF31, ModF32) – помечен как базовый, то в результате рекурсивный спуск затронет лишь модули ModF1, ModF2, ModF3 и вышележащий модуль ModF. Описанный способ организации тестовых наборов с успехом применяется в системах автоматизации тестирования.
Собственно использование эффективной системы автоматизации тестирования сокращает до минимума (например, до одной ночи) время пропуска тестов, без которого невозможно подтвердить факт роста качества (уменьшения числа оставшихся ошибок) продукта. Системное тестирование осуществляется в рамках циклов тестирования (периодов пропуска разработанного тестового набора над build разрабатываемого приложения). Перед каждым циклом фиксируется разработанный или исправленный build, на который заносятся обнаруженные в результате тестового прогона ошибки. Затем ошибки исправляются, и на очередной цикл тестирования предъявляется новый build. Окончание тестирования совпадает с экспериментально подтвержденным заключением о достигнутом уровне качества относительно выбранного критерия тестирования или о снижении плотности не обнаруженных ошибок до некоторой заранее оговоренной величины. Возможность ограничить цикл тестирования пределом в одни сутки или несколько часов поддерживается исключительно за счет средств автоматизации тестирования.

Рис. 8.1. Структура инструментальной системы автоматизации тестирования
На рис. 8.1 представлена обобщенная структура системы автоматизации тестирования, в которой создается и сохраняется следующая информация:
- Набор тестов, достаточный для покрытия тестируемого приложения в соответствии с выбранным критерием тестирования – как результат ручной или автоматической разработки (генерации) тестовых наборов и драйвер/монитор пропуска тестового набора.
- Результаты прогона тестового набора, зафиксированные в Log-файле. Log-файл содержит трассы ("протоколы"), представляющие собой реализованные при тестовом прогоне последовательности некоторых событий (значений отдельных переменных или их совокупностей) и точки реализации этих событий на графе программы.
В составе трасс могут присутствовать последовательности явно и неявно заданных меток, задающих пути реализации трасс на управляющем графе программы, совокупности значений переменных на этих метках, величины промежуточных результатов, достигнутых на некоторых метках и т.п. - Статистика тестового цикла, содержащая: 1) результаты пропуска каждого теста из тестового набора и их сравнения с эталонными величинами; 2) факты, послужившие основанием для принятия решения о продолжении или окончании тестирования; 3) критерий покрытия и степень его удовлетворения, достигнутая в цикле тестирования.
Издержки тестирования
Интенсивность обнаружения ошибок на единицу затрат и надежность тесно связаны со временем тестирования и, соответственно, с гарантией качества продукта (рис. 8.2A ). Чем больше трудозатрат вкладывается в процесс тестирования, тем меньше ошибок в продукте остается незамеченными. Однако совершенство в индустриальном программировании имеет пределы, которые прежде всего связаны с затратами на получение программного продукта, а также с избытком качества, которое не востребовано заказчиком приложения. Нахождение оптимума – очень ответственная задача тестировщика и менеджера проекта.
Движение к уменьшению числа оставшихся ошибок или к качеству продукта приводит к применению различных методов отладки и тестирования в процессе создания продукта. На рис.8.2В приведен затратный компонент тестирования в зависимости от совершенствования применяемого инструментария и методов тестирования. На практике популярны следующие методы тестирования и отладки, упорядоченные по связанным с их применением затратам:
- Статические методы тестирования
- Модульное тестирование
- Интеграционное тестирование
- Системное тестирование
- Тестирование реального окружения и реального времени. Зависимость эффективности применения перечисленных методов или их способности к обнаружению соответствующих классов ошибок (С) сопоставлена на рис. 8.2 с затратами (B). График показывает, что со временем, по мере обнаружения более сложных ошибок и дефектов, эффективность низкозатратных методов падает вместе с количеством обнаруживаемых ошибок.

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

Качество программного продукта и тестирование
Фазы процесса тестирования
В процессе тестирования выделяют следующие фазы:
- Определение целей (требований к тестированию), включающее следующую конкретизацию: какие части системы будут тестироваться, какие аспекты их работы будут выбраны для проверки, каково желаемое качество и т.п.
- Планирование: создание графика (расписания) разработки тестов для каждой тестируемой подсистемы; оценка необходимых человеческих, программных и аппаратных ресурсов; разработка расписания тестовых циклов. Важно отметить, что расписание тестирования обязательно должно быть согласовано с расписанием разработки создаваемой системы, поскольку наличие исполняемой версии разрабатываемой системы (Implementation Under Testing (IUT) или Application Under Testing (AUT) – часто употребляемые обозначения для тестируемой системы) является одним из необходимых условий тестирования, что создает взаимозависимость в работе команд тестировщиков и разработчиков.
- Разработка тестов , то есть тестового кода для тестируемой системы, если необходимо - кода системы автоматизации тестирования и тестовых процедур (выполняемых вручную).
- Выполнение тестов: реализация тестовых циклов.
- Анализ результатов.
Генерация тестов
В настоящее время некоторые языки спецификаций, используемые для описания алгоритмов тестирования, могут быть использованы для генерации тестового кода. Рассмотрим генерацию кода из языка MSC. Тест, описанный выше, формализован на языке MSC (Рис. 9.3). Здесь каждая стрелка с пометкой DoTransaction, DoAddressTenure или DoDataTenure представлет собой вызов соответствующей функции продукта с передачей параметров. Стрелка checkTr соответствует проверке прохождения по шине транзакции с соответствующими параметрами. Каждая из стрелок диаграммы генератором тестов преобразуется в исполнимый код, при этом стрелкам, представляющим собой вызовы функций может соответствовать достаточно простой и маленький участок кода, вызывающий соответствующую функцию и проверяющий ее выходное значение на наличие ошибок.

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

Рис. 9.4. Формальная запись сценарного теста на MSC с использованием параллелизма.
В MSC на Рис. 9.4 проверки транзакций сгруппированы с порождающими их вызовами в отдельные фрагменты, а параллелизм, используемый при исполнении фрагментов, задан через Par – формальную конструкцию, применяемую для изображения параллелизма в языке MSC.
При генерации тестов по диаграмме Рис. 9.4 тестовый генератор перебирает все возможные и неповторяющиеся варианты вызова тестируемых функций, сохраняя при этом корректность порядка проверок, что в данном примере дает три сгенерированных теста. Несложно видеть, что затраты на создание диаграммы Рис. 9.4 не сильно отличаются от затрат на диаграмму Рис. 9.3, в то время как количество тестов увеличивается в три раза.
Таким образом, использование методики генерации тестового кода по формализованным MSC диаграммам позволяет значительно поднять производительность тестирования, а также преобразовать формализацию (кодировку) сценариев в достаточно интеллектуальную деятельность.
![]() |
![]() |
![]() |

Качество программного продукта и тестирование
Качество программного продукта можно оценить некоторым набором характеристик, определяющих, насколько продукт "хорош" с точки зрения всех потенциально заинтересованных в нем сторон. Такими сторонами являются:
- заказчик продукта
- спонсор
- конечный пользователь
- разработчики продукта
- тестировщики продукта
- инженеры поддержки
- отдел обучения
- отдел продаж и т.п.
В рамках подобной задачи группа тестирования рассматривается не просто как еще одна заинтересованная сторона, но и как сторона, способная оценить удовлетворение выбранных критериев и сделать вывод о качестве продукта с точки зрения других участников. К сожалению, далеко не все критерии могут быть оценены группой тестирования. Поэтому ее внимание в основном сосредоточено на критериях, определяющих качество программного продукта с точки зрения конечного пользователя.
Тестирование как способ обеспечения качества. Тестирование, с технической точки зрения, есть процесс выполнения приложения на некоторых входных данных и проверка получаемых результатов с целью подтвердить их корректность по отношению к результату.
Тестирование не позиционируется в качестве единственного способа обеспечения качества. Оно является частью общей системы обеспечения качества продукта, элементы которой выбираются по критерию наибольшей эффективности применения в конкретном проекте.
Рассмотрим пример. В качестве приложения возьмем программу для работы с сетью (browser), критерии качества которой приведены в Табл.9.1.
| + | - | - |
| - | + | - |
| + | Косвенно | + |
| + | - | - |
| - | Косвенно | + |
| - | - | + |
| - | - | + |
| - | + | - |
| ТестированиеАнализ рынка и специальные лаборатории1)Обзоры кодаАнализ дизайнаАудиты процесса разработки | ||||
| +, не всегда эффективно | + | - | - | - |
| - | - | - | - | + |
| + | - | + | - | - |
| +, не всегда эффективно | + | - | - | - |
| - | - | +- | + | - |
| - | - | + | + | - |
| - | - | + | - | + |
| - | - | - | - | + |
В каждом конкретном проекте элементы системы должны быть выбраны так, чтобы обеспечить приемлемое качество, исходя из приоритетов и имеющихся ресурсов. Выбирая элементы для системы обеспечения качества конкретного продукта, можно применить комбинированное тестирование, обзоры кода, аудит. При подобном выборе некоторые качества, например легкость модификации и исправления дефектов, не будут оценены и, возможно, выполнены.
Задачей тестирования в рассматриваемом случае будет обнаружение дефектов и оценка удобства использования продукта, включая полноту функциональности. Исходя из задач, поставленных перед группой тестирования в конкретном проекте, выбирается соответствующая стратегия тестирования. Так, в данном примере, ввиду необходимости оценить удобство использования и полноту функциональности, преимущественный подход к разработке тестов следует планировать на основе использования сценариев.
Итак, основная последовательность действий при выборе и оценке критериев качества программного продукта включает:
- Определение всех лиц, так или иначе заинтересованных в исполнении и результатах данного проекта.
- Определение критериев, формирующих представление о качестве для каждого из участников.
- Приоритезацию критериев, с учетом важности конкретного участника для компании, выполняющей проект, и важности каждого из критериев для данного участника.
- Определение набора критериев, которые будут отслежены и выполнены в рамках проекта, исходя из приоритетов и возможностей проектной команды. Постановка целей по каждому из критериев.
- Определение способов и механизмов достижения каждого критерия.
- Определение стратегии тестирования исходя из набора критериев, попадающих под ответственность группы тестирования, выбранных приоритетов и целей
Подходы к разработке тестов
Рассмотрим разные подходы к разработке тестов, два к выбору тестовых данных и два к реализации тестового кода.
Пример использования спецификации требований для разработки тестов.
Так, для рассмотренного выше примера возможно создание следующего сценария и тестов.
- Сценарий: пользователь имеет две независимые нити управления, одна из которых отвечает за генерацию полных транзакций посредством DoTransaction, а другая – за сбор транзакций из адресной части и части данных, когда эта информация приходит из разных источников. Таким образом, вторая нитка использует вызовы к DoAddressTenure и DoDataTenure.
- Описание тестов: Вызвать DoAddressTenure c адресом А1, вызвать DoTransaction с адресом А2 и данными D2, вызвать DoDataTenure с данными D1. Проверить последовательное появление на шине двух транзакций: {А1, D1} и {А2, D2}
Пример использования спецификации требований для разработки тестов.
Пусть задан следующий фрагмент набора требований для модели обмена транзакциями:
- Функция DoTransaction должна принимать адрес и данные в соответствии с параметрами, создавать в очереди новый элемент, заполнять его адресную часть и часть полей данных переданной информацией и инициировать транзакцию
- Функция DoAddressTenure должна принимать адрес в соответствии с параметрами, создавать в очереди новый элемент и заполнять его адресную часть
- Функция DoDataTenure должна принимать данные в соответствии с параметрами, находить в очереди первый элемент с частично незаполненными полями данных, дополнять его переданной информацией и инициировать транзакцию
- Вызвать DoTransaction с адресом и данными. Проверить появление в очереди еще одного элемента. Проверить появление на шине транзакции с правильными адресом и данными.
- Вызвать DoAddressTenure с адресом. Проверить появление в очереди еще одного элемента. Проверить отсутствие новой транзакции на шине.
- Вызвать DoDataTenure с данными. Проверить заполнение полей данных. Проверить появление на шине транзакции с правильными адресом и данными
Процесс тестирования
Как отмечалось в подразделе 2.4, в тестировании выделяются три основных уровня, или три фазы:
- Модульное тестирование.
- Интеграционное тестирование.
- Системное тестирование.
Ручная разработка тестов
Наиболее распространенным способом разработки тестов является создание тестового кода вручную. Это наиболее гибкий способ разработки тестов, однако характерная для него производительность труда инженеров-тестировщиков в создании тестового кода не намного выше скорости создания кода продукта, а объемы тестового кода на практике зачастую превышают объем кода продукта в 10 раз. Учитывая этот факт, в современной индустрии все больше склоняются к более интеллектуальным способам получения тестового кода, таким как использование специальных тестовых языков (скриптов) и генерации тестов.
Тестирование сценариев
Разработка тестов, основанных на использовании сценариев, осуществляется по следующей методике:
- Определяется модель использования, включающая операционное окружение продукта и "актеров". Актером может быть пользователь, другой продукт, аппаратная часть и тому подобное, то есть все, с чем продукт обменивается информацией. Разделение на окружение и актеров условно и служит для описания оптимальных способов использования продукта.
- Разрабатываются сценарии использования продукта. Описание сценария в зависимости от продукта и выбранного подхода может быть строго определенным, параметризованным или разрешать некоторую степень неопределенности. Например, описание сценария на языке MSC допускает задание параметризованных сценариев с возможностью переупорядочивания событий.
- Разрабатывается набор тестов, покрывающих заданные сценарии. С учетом степени неопределенности, заложенной в сценарии, каждый тест может покрывать один сценарий, несколько сценариев, или, наоборот, часть сценария.
Еще одна особенность тестирования сценариев заключается в том, что этот метод направляет тестирование на проверку конкретных режимов использования продукта, что позволяет находить дефекты, которые метод тестирования по требованиям может пропустить.
Тестирование спецификации
При разработке тестов, основанной на функциональной спецификации продукта, требования к продукту являются основным источником, определяющим, какие тесты будут разработаны. Для каждого требования пишется один или более тестов, которые в совокупности должны проверить выполнение данного требования в продукте.
Тестовый цикл
Тестовый цикл – это цикл исполнения тестов, включающий фазы 4 и 5 тестового процесса. Тестовый цикл заключается в прогоне разработанных тестов на некотором однозначно определяемом срезе системы (состоянии кода разрабатываемой системы). Обычно такой срез системы называют build. Тестовый цикл включает следующую последовательность действий:
-
Проверка готовности системы и тестов к проведению тестового цикла включающая:
- Проверку того, что все тесты, запланированные для исполнения на данном цикле, разработаны и помещены в систему версионного контроля.
- Проверку того, что все подсистемы, запланированные для тестирования на данном цикле, разработаны и помещены в систему версионного контроля.
- Проверку того, что разработана и задокументирована процедура определения и создания среза системы, или build.
- Проверки некоторых дополнительных критериев.
- Подготовка тестовой машины в соответствии с требованиями, определенными на этапе планирования (например, полная очистка и переустановка системного программного обеспечения). Конфигурация тестовой машины, так же, как и срез системы, должны быть однозначно воспроизводимыми.
- Воспроизведение среза системы.
- Прогон тестов в соответствии с задокументированными процедурами
- Сохранение тестовых протоколов (test log). Test log может содержать вывод системы в STDOUT, список результатов сравнения полученных при исполнении данных с эталонными или любые другие выходные данные тестов, с помощью которых можно проверить правильность работы системы
- Анализ протоколов тестирования и принятие решения о том прошел или не прошел каждый из тестов (Pass/Fail)
- Анализ и документирование результатов цикла
Тестовый план
Тестовый план - это документ, или набор документов, содержащий следующую информацию:
- Тестовые ресурсы.
- Перечень функций и подсистем, подлежащих тестированию.
-
Тестовую стратегию, включающую:
- Анализ функций и подсистем с целью определения наиболее слабых мест, то есть областей функциональности тестируемой системы, где появление дефектов наиболее вероятно.
- Определение стратегии выбора входных данных для тестирования. Так как множество возможных входных данных программного продукта, как правило, практически бесконечно, выбор конечного подмножества, достаточного для проведения исчерпывающего тестирования, является сложной задачей. Для ее решения могут быть применены такие методы, как покрытие классов входных и выходных данных, анализ крайних значений, покрытие модели использования, анализ временной линии и тому подобные. Выбранную стратегию необходимо обосновать и задокументировть.
- Определение потребности в автоматизированной системе тестирования и дизайн такой системы
- Расписание тестовых циклов (пример приведен на Рис. 9.1).
- Фиксацию тестовой конфигурации: состава и конкретных параметров аппаратуры и программного окружения (пример приведен на Рис. 9.2).
- Определение списка тестовых метрик, которые на тестовом цикле необходимо собрать и проанализировать. Например, метрик, оценивающих степень покрытия тестами набора требований, степень покрытия кода тестируемой системы, количество и уровень серьезности дефектов, объем тестового кода и другие характеристики.

Рис. 9.1. Пример расписания двух последних тестовых циклов

Рис. 9.2. Пример детализации условий проведения системных циклов
Типы тестирования
В тестовом плане определяются и документируются различные типы тестов. Типы тестов могут быть классифицированы по двум категориям: по тому, что подвергается тестированию (по виду подсистемы) и по способу выбора входных данных.
Типы тестирования по виду подсистемы или продукта:
- Тестирование основной функциональности, когда тестированию подвергается собственно система, являющаяся основным выпускаемым продуктом
- Тестирование инсталляции включает тестирование сценариев первичной инсталляции системы, сценариев повторной инсталляции (поверх уже существующей копии), тестирование деинсталляции, тестирование инсталляции в условиях наличия ошибок в инсталлируемом пакете, в окружении или в сценарии и т.п.
- Тестирование пользовательской документации включает проверку полноты и понятности описания правил и особенностей использования продукта, наличие описания всех сценариев и функциональности, синтаксис и грамматику языка, работоспособность примеров и т.п.
-
Функциональное тестирование, при котором проверяется:
- Покрытие функциональных требований.
- Покрытие сценариев использования.
- Стрессовое тестирование, при котором проверяются экстремальные режимы использования продукта.
- Тестирование граничных значений.
- Тестирование производительности.
- Тестирование на соответствие стандартам.
- Тестирование совместимости с другими программно-аппаратными комплексами.
- Тестирование работы с окружением.
- Тестирование работы на конкретной платформе
Выполнение тестов
Автоматизированное тестирование
Попытка автоматизировать приведенный выше тест приводит к созданию скрипта, задающего тестируемому продукту три конкретных числа и перенаправляющего вывод продукта в файл с целью его анализа, а также содержащего конкретное значение желаемого результата, с которым сверяется получаемое при прогоне теста значение. Таким образом, вся необходимая информация должна быть явно помещена в текст (скрипт) теста, что требует дополнительных по сравнению с ручным подходом усилий. Также дополнительных усилий и времени требует создание разборщика вывода (программы согласования форматов представления эталонных значений из теста и вычисляемых при прогоне результатов) и, возможно, создание базы хранения состояний эталонных данных.
Документирование и жизненный цикл дефекта
Каждый дефект, обнаруженный в процессе тестирования, должен быть задокументирован и отслежен. При обнаружении нового дефекта его заносят в базу дефектов. Для этого лучше всего использовать специализированные базы, поддерживающие хранение и отслеживание дефектов - типа DDTS . При занесении нового дефекта рекомендуется указывать, как минимум, следующую информацию:
- Наименование подсистемы, в которой обнаружен дефект.
- Версия продукта (номер build ), на котором дефект был найден.
- Описание дефекта.
- Описание процедуры (шагов, необходимых для воспроизведения дефекта).
- Номер теста, на котором дефект был обнаружен.
- Уровень дефекта, то есть степень его серьезности с точки зрения критериев качества продукта или заказчика.
- Причину возникновения дефекта.
- Место исправления, как минимум, с точностью до исправленного файла.
- Краткое описание того, что было исправлено.
- Время, затраченное на исправление.
Если проектная команда принимает решение о том, что некоторый дефект исправляться не будет, то такой дефект переводится в состояние "Postponed" с указанием лиц, ответственных за это решение, и причин его принятия.
Обзоры тестов и стратегии
Тестовый код и стратегия тестирования, зафиксированные в виде документов, заметно улучшаются, если подвергаются коллективному обсуждению. Такие обсуждения называются обзорами (review). Существует принятая в организации процедура проведения и оценки результатов обзора. Обзоры наряду с тестированием образуют мощный набор методов борьбы с ошибками с целью повышения качества продукта. Цели обзоров тестовой стратегии и тестового кода различны.
Цели обзора тестовой стратегии:
- Установить достаточность проверок, обеспечиваемых тестированием.
- Проанализировать оптимальность покрытия или адекватность распределения количества планируемых тестов по функциональности продукта
- Проанализировать оптимальность подхода к разработке кода, генерации кода, автоматизации тестирования.
- Установить соответствие тестового набора тестовой стратегии.
- Проверить правильность кодирования тестов.
- Оценить достигнутую степень качества кода, исходя из требований по стандартам, простоте поддержки, наличию комментариев и т.п.
- Если необходимо, проанализировать оптимальность тестового кода с целью удовлетворения требований к быстродействию и объему.

Оценка качества тестов
Тесты нуждаются в контроле качества так же, как и тестируемый продукт. Поскольку тесты для продукта являются своего рода эталоном его структурных и поведенческих характеристик, закономерен вопрос о том, насколько адекватен эталон. Для оценки качества тестов используются различные методы, наиболее популярные из которых кратко рассмотрены ниже.
Описание тестов
Описание тестов разрабатывается для облегчения анализа и поддержки тестового набора. Описание может быть реализовано в произвольной форме, но при этом должны выполнять следующие задачи:
- Анализировать степень покрытия продукта тестами на основании описания тестового набора.
- Для любой функции тестируемого продукта найти тесты, в которых функция используется.
- Для любого теста определить все функции и их сочетания, которые данный тест использует (затрагивает).
- Понять структуру и взаимосвязи тестовых файлов.
- Понять принцип построения системы автоматизации тестирования
Пример фрагмента процедуры
- Подать на вход три разных целых числа.
- Запустить тестовое исполнение.
- Проверить, соответствует ли полученный результат таблице [ссылка на документ1] с учетом поправок [ссылка на документ2].
- Убедиться в понятности и корректности выдаваемой сопроводительной информации
Пример скрипта
Приведем пример последовательности действий, закладываемых в скрипт:
- Выдать на консоль имя или номер теста и время его начала
- Вызвать продукт с фиксированными параметрами
- Перенаправить вывод продукта в файл
- Проверить возвращенное продуктом значение. Оно должно быть равно ожидаемому (эталонному) результату, эафиксированному в тесте.
- Проверить вывод продукта, сохраненный в файле (п.3), на равенство заранее приготовленному эталону
- Выдать на консоль результаты теста в виде вердикта PASS/FAIL и в случае FAIL - краткого пояснения, какая именно проверка не прошла
- Выдать на консоль время окончания теста
Ручное тестирование
Ручное тестирование заключается в выполнении задокументированной процедуры, где описана методика выполнения тестов, задающая порядок тестов и для каждого теста - список значений параметров, который подается на вход, и список результатов, ожидаемых на выходе. Поскольку процедура предназначена для выполнения человеком, в ее описание для краткости могут использоваться некоторые значения по умолчанию, ориентированные на здравый смысл, или ссылки на информацию, хранящуюся в другом документе.
Сравнение ручного и автоматизированного тестирования
Результаты сравнения приведены в Табл. 10.1. Сравнение показывает тенденцию современного тестирования, ориентирующую на максимальную автоматизацию процесса тестирования и генерацию тестового кода, что позволяет справляться с большими объемами данных и тестов, необходимых для обеспечения качества при производстве программных продуктов.
| РучноеАвтоматизированное | |
| Гибкость в задании данных. Позволяет использовать разные значения на разных циклах прогона тестов, расширяя покрытие | Входные значения строго заданы |
| Гибкая, позволяет тестировщику оценивать нечетко сформулированные критерии | Строгая. Нечетко сформулированные критерии могут быть проверены только путем сравнения с эталоном |
| Низкая. Человеческий фактор и нечеткое определение данных приводят к неповторяемости тестирования | Высокая |
| Низкая. Длительные тестовые циклы приводят к снижению внимания тестировщика | Высокая, не зависит от длины тестового цикла |
| Зависит от детальности описания процедуры. Обычно тестировщик в состоянии выполнить тест, если внешний вид продукта и текст сообщений несколько изменились | Высокая. Незначительные изменения в интерфейсе часто ведут к коррекции эталонов |
| Низкая | Высокая |
| Отсутствует. Низкая скорость выполнения обычно не позволяет исполнить сгенерированный набор тестов | Поддерживается |
Тестовые метрики
Существует устоявшийся набор тестовых метрик, который помогает определить эффективность тестирования и текущее состояние продукта. К таким метрикам относятся следующие:
- Покрытие функциональных требований.
- Покрытие кода продукта. Наиболее применимо для модульного уровня тестирования.
- Покрытие множества сценариев.
- Количество или плотность найденных дефектов. Текущее количество дефектов сравнивается со средним для данного типа продуктов с целью установить, находится ли оно в пределах допустимого статистического отклонения. При этом обнаруженные отклонения как в большую, так и в меньшую сторону приводят к анализу причин их появления и, если необходимо, к выработке корректирующих действий.
- Соотношение количества найденных дефектов с количеством тестов на данную функцию продукта. Сильное расхождение этих двух величин говорит либо о неэффективности тестов (когда большое количество тестов находит мало дефектов) либо о плохом качестве данного участка кода (когда найдено большое количество дефектов на не очень большом количестве тестов).
- Количество найденных дефектов, соотнесенное по времени, или скорость поиска дефектов. Если производная такой функции близка к нулю, то продукт обладает качеством, достаточным для окончания тестирования и поставки заказчику.
Тестовые процедуры
Тестовые процедуры - это формальный документ, содержащий описание необходимых шагов для выполнения тестового набора. В случае ручных тестов тестовые процедуры содержат полное описание всех шагов и проверок, позволяющих протестировать продукт и вынести вердикт PASS/FAIL.

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

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

Рис. 10.3. Фрагмент тестового отчета
Выполнение тестов
Рассмотрим два основных подхода к выполнению тестов: подход ручного тестирования и подход автоматического исполнения (прогон) тестов. Подходы рассмотрены на примере тестирования продукта, поддерживающего интерфейс командной строки. Тесты описывают вызов продукта с параметрами и проверку возвращаемого значения в виде фиксируемых при прогоне – текста из STDOUT и состояния некоторых файлов, зависящего от входных параметров.
Цели и задачи регрессионного тестирования
Цели и задачи регрессионного тестирования
При корректировках программы необходимо гарантировать сохранение качества. Для этого используется регрессионное тестирование - дорогостоящая, но необходимая деятельность в рамках этапа сопровождения, направленная на перепроверку корректности измененной программы. В соответствии со стандартным определением, регрессионное тестирование - это выборочное тестирование, позволяющее убедиться, что изменения не вызвали нежелательных побочных эффектов, или что измененная система по-прежнему соответствует требованиям.
Главной задачей этапа сопровождения является реализация систематического процесса обработки изменений в коде. После каждой модификации программы необходимо удостовериться, что на функциональность программы не оказал влияния модифицированный код. Если такое влияние обнаружено, говорят о регрессионном дефекте. Для регрессионного тестирования функциональных возможностей, изменение которых не планировалось, используются ранее разработанные тесты. Одна из целей регрессионного тестирования состоит в том, чтобы, в соответствии с используемым критерием покрытия кода (например, критерием покрытия потока операторов или потока данных), гарантировать тот же уровень покрытия, что и при полном повторном тестировании программы. Для этого необходимо запускать тесты, относящиеся к измененным областям кода или функциональным возможностям.
Пусть T = {t1, t2, ..., tN} - множество из N тестов, используемое при первичной разработке программы P, а T'


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




Реалистичный вариант решения задачи выборочного регрессионного тестирования состоит в получении полезной информации по результатам выполнения P и объединения этой информации с данными статического анализа для получения множества T'реальное в виде аппроксимации T'идеальное. Этот подход применяется во всех известных выборочных методах регрессионного тестирования, основанных на анализе кода.
Множество T' реальное должно включать все тесты из T, активирующие измененный код, и не включать никаких других тестов, то есть тест t

Если некоторый тест t задействует в P тот же код, что и в P', выходные данные P и P' для t различаться не будут. Из этого следует, что если P(t)



Важной задачей регрессионного тестирования является также уменьшение стоимости и сокращение времени выполнения тестов.
Рассмотрим отбор тестов на примере рис. 11.1. Код, покрываемый тестами, выделен цветом и штриховкой. Легко заметить, что код, покрываемый тестом 1, не изменился с предыдущей версии, следовательно, повторное выполнение теста 1 не требуется. Напротив, код, покрываемый тестами 2, 3 и 4, изменился; следовательно, требуется их повторный запуск.

Рис. 11.1. Отбор тестов для множества T'.
Классификация тестов при отборе
Создание наборов регрессионных тестов рекомендуется начинать с множества исходных тестов. При заданном критерии регрессионного тестирования все исходные тесты t (t

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

Рис. 11.2. Жизненный цикл теста
Классификация тестов по отношению к изменениям в коде требует анализа последствий изменений. Тесты, активирующие код, затронутый изменениями, могут требовать повторного запуска или оказаться устаревшими. Чтобы тест был включен в класс тестов, требующих повторного запуска, он должен быть затронут изменениями в коде, а также должен способствовать увеличению степени покрытия измененного кода по используемому критерию. Затронутым элементом теста может быть траектория, выходные значения, или и то, и другое. Чтобы тест был включен в класс тестов, пригодных для повторного использования, он должен вносить вклад в увеличение степени покрытия кода и не требовать повторного запуска.
Степень покрытия кода определяется для тестов, пригодных для повторного использования, поскольку к этому классу относятся тесты, не требующие повторного запуска и способствующие увеличению степени покрытия до желаемой величины. Если имеется компонент программы, не задействованный пригодными для повторного использования тестами, то вместо них выбираются и выполняются с целью увеличения степени покрытия тесты, требующие повторного запуска. После запуска такой тест становится пригодным для повторного использования или устаревшим. Если тестов, требующих повторного запуска, больше не осталось, а необходимая степень покрытия кода еще не достигнута, порождаются дополнительные тесты и тестирование повторяется.
Окончательный набор тестов собирается из тестов, пригодных для повторного использования, тестов, требующих повторного запуска, и новых тестов. Наконец, устаревшие и избыточные тесты удаляются из набора тестов, поскольку избыточные тесты не проверяют новые функциональные возможности и не увеличивают покрытие.
Классификация выборочных методов
Для проверки корректности различных подходов к регрессионному тестированию используется модель оценки методов регрессионного тестирования. Основными объектами рассмотрения стали полнота, точность, эффективность и универсальность.
| 1 | 1 | -6 | 1 | 2 | -3 | 25 | Solution: X1 = 2, X2 = -3 |
| 2 | -3 | 5 | 1 | 0.75 | 5.567764 | -31 | Solution: X1 = 0.75+5.567764i, X2 = 0.75-5.567764I |
| 1 | 2 | 0 | 0 | 0 | -2 | 4 | |
| 1 | 2 | 1 | 0 | -1 | -1 | 0 | |
| 1 | 2 | 2 | 0 | -1 | 2 | -4 |
Точность - мера способности метода избегать выбора тестов из T, на которых результат выполнения измененной программы не будет отличаться от результата ее первоначальной версии, то есть тестов, неспособных обнаруживать ошибки в P'. Предположим, что набор T содержит r регрессионных тестов. Из них для n тестов (n




Исходя из приведенного определения, точность множества тестов - это отношение числа тестов данного множества, на которых результаты выполнения новой и старой программ различаются, к общему числу тестов множества. Точность является важным атрибутом метода регрессионного тестирования. Неточный метод имеет тенденцию отбирать тесты, которые не должны были быть выбраны. Чем менее точен метод, тем ближе объем выбранного набора тестов к объему исходного набора тестов.
Эффективность - оценка вычислительной стоимости стратегии выборочного регрессионного тестирования, то есть стоимости реализации ее требований по времени и памяти, а также возможности автоматизации. Относительной эффективностью называется эффективность метода тестирования при условии наличия не более одной ошибки в тестируемой программе. Абсолютной эффективностью называется эффективность метода в реальных условиях, когда оценка количества ошибок в программе не ограничена.
Универсальность отражает меру способности метода к применению в достаточно широком диапазоне ситуаций, встречающихся на практике.
Для программы P, ее измененной версии P' и набора тестов T для P требуется, чтобы методика выборочного повторного тестирования удовлетворяла следующим критериям оценки:
- Критерий 1 - Безопасность. Методика выборочного повторного тестирования должна быть безопасной, то есть должна выбирать все тесты из T, которые потенциально могут обнаруживать ошибки (все тесты, чье поведение на P' и P может быть различным). Безопасная методика должна рассматривать последствия добавления, удаления и изменения кода. При добавлении нового кода в P в T могут уже содержаться тесты, покрывающие этот новый код. Такие тесты необходимо обнаруживать и учитывать при отборе.
- Критерий 2 - Точность. Стратегия повторного прогона всех тестов является безопасной, но неточной. В дополнение к выбору всех тестов, потенциально способных обнаруживать ошибки, она также выбирает тесты, которые ни в коем случае не могут демонстрировать измененное поведение. В идеале, методика выборочного повторного тестирования должна быть точной, то есть должна выбирать только тесты с изменившимся поведением. Однако для произвольно взятого теста, не запуская его, невозможно определить, изменится ли его поведение. Следовательно, в лучшем случае мы можем рассчитывать лишь на некоторое увеличение точности.
Всевозможные существующие выборочные методы регрессионного тестирования различаются не в последнюю очередь выбором объекта или объектов, для которых выполняется анализ покрытия и анализ изменений. Например, при анализе на уровне функции при изменении любого оператора функции вся функция считается измененной; при анализе на уровне отдельных операторов мы можем исключить часть тестов, содержащих вызов функции, но не активирующих измененный оператор. Выбор объектов для анализа покрытия отражается на уровне подробности анализа, а значит, и на его точности и эффективности. Абсолютные величины точности и количества выбранных тестов для заданных набора тестов и множества изменений должны рассматриваться только вместе с уменьшением размера набора тестов. Небольшой процент выбранных тестов может быть приемлемым, только если уровень точности остается достаточно высоким. - Критерий 3 - Эффективность. Методика выборочного повторного тестирования должна быть эффективной, то есть должна допускать автоматизацию и выполняться достаточно быстро для практического применения в условиях ограниченного времени регрессионного тестирования. Методика должна также предусматривать хранение информации о ходе исполнения тестов в минимально возможном объеме.
- Критерий 4 - Универсальность. Методика выборочного повторного тестирования должна быть универсальной, то есть применимой ко всем языкам и языковым конструкциям, эффективной для реальных программ и способной к обработке сколь угодно сложных изменений кода.
В общем случае существует некоторый компромисс между безопасностью, точностью и эффективностью. При отборе тестов анализ необходимо провести за время, меньшее, чем требуется для выполнения и проверки результатов тестов из T, не вошедших в T'. С учетом этого ограничения решением задачи регрессионного тестирования будет безопасный метод с хорошим балансом дешевизны и высокой точности.

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

- Код, находящийся на одном пути с измененным кодом, может не воздействовать на значения выходных данных измененных модулей программы.
- Не всегда каждый оператор программы воздействует на каждый элемент ее выходных данных.
Следовательно, необходимо ориентироваться на выбор только тех тестов, которые покрывают измененный код, воздействующий, в свою очередь, на вывод программы. Такой подход гарантирует, что будут выбраны только тесты, обнаруживающие изменения, и метод будет, как говорят, точным.
Пример регрессионного тестирования функции решения квадратного уравнения.
Код этой функции приведен на пример 11.1. Входными параметрами являются коэффициенты квадратного уравнения A, B и C, а также флаг Print, ненулевое значение которого указывает, что полученное решение необходимо вывести на экран. К выходным параметрам относятся X1 и X2, предназначенные для хранения корней уравнения, и возвращаемое значение функции - дискриминант уравнения. В исходном виде функция содержит дефект, в результате чего уравнения с отрицательным дискриминантом порождают ошибку времени выполнения. В новой версии функции дефект должен быть исправлен; кроме того, необходимо реализовать запрос пользователя на изменение формата вывода решения. Код новой версии функции Equation приводится на пример 11.2.
Существующие тесты для функции Equation приведены в таблица 11.2. Входные данные тестов представляют собой совокупность значений Print, A, B и C, подаваемых на вход функции. Выходными данными для теста являются значения X1 и X2, возвращаемое значение функции, а также строка, выводимая на экран; в таблица 11.2 приведены ожидаемые значения выходных данных. Кроме того, для каждого теста вычисляется траектория его прохождения по коду.
double Equation(int Print, float A, float B, float C, float& X1, float& X2) { float D = B * B - 4.0 * A * C; if (D >= 0) { X1 = (-B + sqrt(D)) / 2.0 / A; X2 = (-B - sqrt(D)) / 2.0 / A; } else { X1 = -B / 2.0 / A; X2 = sqrt(D); } if (Print) printf("Solution: %f, %f\n", X1, X2); return D; } Пример 11.1. Функция Equation - исходная версия. double Equation(int Print, float A, float B, float C, float& X1, float& X2) { float D = B * B - 4.0 * A * C; if (D >= 0) { X1 = (-B + sqrt(D)) / 2.0 / A; X2 = (-B - sqrt(D)) / 2.0 / A; } else { X1 = -B / 2.0 / A; X2 = sqrt(D); } if (Print) printf("Solution: %f, %f\n", X1, X2); return D; } Пример 11.1.1. Функция Equation - исходная версия. double Equation(int Print, float A, float B, float C, float& X1, float& X2) { float D = B * B - 4.0 * A * C; if (D >= 0) { X1 = (-B + sqrt(D)) / 2.0 / A; X2 = (-B - sqrt(D)) / 2.0 / A; } else { X1 = -B / 2.0 / A; X2 = sqrt(-D); } if (Print) { if (D >= 0) printf("Solution: X1 = %f, X2 = %f\n", X1, X2); else printf("Solution: X1 = %f+%fi, X2 = %f-%fi\n", X1, X2, X1, X2); } return D; } Пример 11.2. Функция Equation - измененная версия. double Equation(int Print, float A, float B, float C, float& X1, float& X2) { float D = B * B - 4.0 * A * C; if (D >= 0) { X1 = (-B + sqrt(D)) / 2.0 / A; X2 = (-B - sqrt(D)) / 2.0 / A; } else { X1 = -B / 2.0 / A; X2 = sqrt(-D); } if (Print) { if (D >= 0) printf("Solution: X1 = %f, X2 = %f\n", X1, X2); else printf("Solution: X1 = %f+%fi, X2 = %f-%fi\n", X1, X2, X1, X2); } return D; } Пример 11.2.1. Функция Equation - измененная версия.При изменении функции Equation от пример 11.1 к пример 11.2 меняется формат выводимых на экран данных, так что тесты 1 и 2, проверяющие вывод на экран, могут быть повторно использованы только на уровне 2. Тесты 3, 4 и 5 могут быть использованы на уровне 3 или 4 в зависимости от результатов анализа их траектории.
Управляемое регрессионное тестирование
В течение жизненного цикла программы период сопровождения длится долго. Когда измененная программа тестируется набором тестов T, мы сохраняем без изменений по отношению к тестированию исходной программы P все факторы, которые могли бы воздействовать на вывод программы. Поэтому атрибуты конфигурации, в которой программа тестировалась последний раз (например, план тестирования, тесты tj и покрываемые элементы MT(P,C,tj)), подлежат управлению конфигурацией. Практика тестирования измененной версии программы P' в тех же условиях, в которых тестировалась исходная программа P, называется управляемым регрессионным тестированием. При неуправляемом регрессионном тестировании некоторые свойства методов регрессионного тестирования могут изменяться, например, безопасный метод отбора тестов может перестать быть безопасным. В свою очередь, для обеспечения управляемости регрессионного тестирования необходимо выполнение ряда условий:
- Как при модульном, так и при интеграционном регрессионном тестировании в качестве модулей, вызываемых тестируемым модулем непосредственно или косвенно, должны использоваться реальные модули системы. Это легко осуществить, поскольку на этапе регрессионного тестирования все модули доступны в завершенном виде.
- Информация об изменениях корректна. Информация об изменениях указывает на измененные модули и разделы спецификации требований, не подразумевая при этом корректность самих изменений. Кроме того, при изменении спецификации требований необходимо усиленное регрессионное тестирование изменившихся функций этой спецификации, а также всех функций, которые могли быть затронуты по неосторожности. Единственным случаем когда мы вынуждены положиться на правильность измененного технического задания, является изменение технического задания для всей системы или для модуля верхнего (в графе вызовов) уровня, при условии, что кроме технического задания, не существует никакой дополнительной документации и/или какой-либо другой информации, по которой можно было бы судить об ошибке в техническом задании.
- В программе нет ошибок, кроме тех, которые могли возникнуть из-за ее изменения.
- Тесты, применявшиеся для тестирования предыдущих версий программного продукта, доступны, при этом протокол прогона тестов состоит из входных данных, выходных данных и траектории.
Траектория представляет собой путь в управляющем графе программы, прохождение которого вызывается использованием некоторого набора входных данных. Ее можно применять для оценки структурного покрытия, обеспечиваемого набором тестов. - Для проведения регрессионного тестирования с использованием существующего набора тестов необходимо хранить информацию о результатах выполнения тестов на предыдущих этапах тестирования.
Проблема языков типа C и C++, которые допускают произвольные арифметические операции над указателями, состоит в том, что указатели могут нарушать границы областей памяти, на которые они указывают. Это означает, что переменные могут обрабатываться способами, которые не поддаются анализу на уровне исходного кода. Чтобы учесть такие нарушения границ памяти, выдвигаются следующие гипотезы:
- Гипотеза 1 (Четко определенная память). Каждый сегмент памяти, к которому обращается система программного обеспечения, соответствует некоторой символически определенной переменной.
- Гипотеза 2 (Строго ограниченный указатель). Каждая переменная или выражение, используемое как указатель, должно ссылаться на некоторую базовую переменную и ограничиваться использованием сегмента памяти, определяемого этой переменной.
Существуют и организационные условия проведения регрессионного тестирования. Это ресурс (время), необходимый тестовому аналитику для ознакомления со спецификацией требований системы, ее архитектурой и, возможно, самим кодом.
Виды регрессионного тестирования
Поскольку регрессионное тестирование представляет собой повторное проведение цикла обычного тестирования, виды регрессионного тестирования совпадают с видами обычного тестирования. Можно говорить, например, о модульном регрессионном тестировании или о функциональном регрессионном тестировании.
Другой способ классификации видов регрессионного тестирования связывает их с типами сопровождения, которые, в свою очередь, определяются типами модификаций. Выделяют три типа сопровождения:
- Корректирующее сопровождение, называемое обычно исправлением ошибок, выполняется в ответ на обнаружение ошибки, не требующей изменения спецификации требований. При корректирующем сопровождении производится диагностика и корректировка дефектов в программном обеспечении с целью поддержания системы в работоспособном состоянии.
- Адаптивное сопровождение осуществляется в ответ на требования изменения данных или среды исполнения. Оно применяется, когда существующая система улучшается или расширяется, а спецификация требований изменяется с целью реализации новых функций.
- Усовершенствующее (прогрессивное) сопровождение включает любую обработку с целью повышения эффективности работы системы или эффективности ее сопровождения.
Соответственно, определяют два типа регрессионного тестирования: прогрессивное и корректирующее.
- Прогрессивное регрессионное тестирование предполагает модификацию технического задания.
В большинстве случаев при этом к системе программного обеспечения добавляются новые модули. - При корректирующем регрессионном тестировании техническое задание не изменяется. Модифицируются только некоторые операторы программы и, возможно, конструкторские решения.
Подход к отбору регрессионных тестов может быть активным или консервативным. Активный подход во главу угла ставит уменьшение объема регрессионного тестирования и пренебрегает риском пропустить дефекты. Активный подход применяется для тестирования систем с высокой исходной надежностью, а также в случаях, когда эффект изменений невелик. Консервативный подход требует отбора всех тестов, которые с ненулевой вероятностью могут обнаруживать дефекты. Этот подход позволяет обнаруживать большее количество ошибок, но приводит к созданию более обширных наборов регрессионных тестов.
Возможности повторного использования тестов
К изменению существующих тестов могут привести три следующих вида деятельности программистов:
- Создание новых тестов
- Выполнение тестов
- Изменение кода
В соответствии с приведенными выше рассуждениями можно выделить четыре уровня повторного использования теста:
- Уровень 1: Тест не допускает повторного использования. Требуется создание нового набора тестов (например, путем удаления или изменения этого теста).
- Уровень 2: Повторное использование возможно только входных данных теста. Во многих случаях цель тестирования состоит в активизации некоторых покрываемых элементов программы. Если из траектории существующего теста видно, что элементы программы, подлежащие покрытию, задействуются до измененных команд, входные данные теста могут быть использованы повторно для покрытия этих элементов. В результате изменений в программе и/или техническом задании новая траектория и выходные данные теста могут отличаться от результатов предыдущего выполнения. Таким образом, тесты первого уровня должны быть запущены повторно для получения новых выходных данных и траекторий.
- Уровень 3: Возможно повторное использование как входных, так и выходных данных теста. Очевидно, что на этом уровне обычно располагаются функциональные тесты. Если модуль подвергся только изменению кода с сохранением функциональности, возможно повторное использование существующих функциональных тестов для проверки правильности реализации. Поскольку траектория может измениться, а выходные данные - подвергнуться воздействию со стороны изменений кода, такие тесты должны быть запущены повторно, но ожидается получение идентичных результатов.
- Уровень 4: Наивысший уровень повторного использования теста, предусматривающий повторное использование входных данных, выходных данных и траектории теста. В этом случае на траектории теста не изменяется ни один оператор. Следовательно, в повторном запуске этих тестов необходимости нет, так как выходные данные и траектория останутся неизменными.
Случайные методы
Безопасные методы
Метод выборочного регрессионного тестирования называется безопасным, если при некоторых четко определенных условиях он не исключает тестов (из доступного набора тестов), которые обнаружили бы ошибки в измененной программе, то есть обеспечивает выбор всех тестов, обнаруживающих изменения. Тест называется обнаруживающим изменения, если его выходные данные при прогоне на P' отличаются от выходных данных при прогоне на P: P(t)

Выбор всех выполняющих изменение тестов является безопасным, но при этом отбираются некоторые тесты, не обнаруживающие изменений. Безопасный метод может включать в T' подмножество тестов, выходные данные которых для P и P' ни при каких условиях не отличаются. Поскольку не существует методики, кроме собственно выполнения теста, позволяющей для любой P' определить, будут ли выходные данные теста различаться для P и P', ни один метод не может быть безопасным и абсолютно точным одновременно. T' является безопасным подмножеством T тогда и только тогда, когда:
P(t)



Если P и P' выполняются в идентичных условиях и T' является безопасным подмножеством T, исполнение T' на P' всегда обнаруживает любые связанные с изменениями ошибки в P, которые могут быть найдены путем исполнения T. Если существует тест, обнаруживающий ошибку, безопасный метод всегда находит ее. Таким образом, ни один случайный метод не обладает такой же эффективностью обнаружения ошибок, как безопасный метод.
При некоторых условиях безопасные методы в силу определения "безопасности" гарантируют, что все "обнаруживаемые" ошибки будут найдены. Поэтому относительная эффективность всех безопасных методов равна эффективности метода повторного прогона всех тестов и составляет 100%. Однако их абсолютная эффективность падает с увеличением интервала тестирования. Отметим, что безопасный метод действительно безопасен только в предположении корректности исходного множества тестов T, то есть когда при выполнении всех t

Существуют программы, измененные версии и наборы тестов, для которых применение безопасного отбора не дает большого выигрыша в размере набора тестов. Характеристики исходной программы, измененной версии и набора тестов могут совместно или независимо воздействовать на результаты отбора тестов. Например, при усложнении структуры программы вероятность активации произвольным тестом произвольного изменения в программе уменьшается. Безопасный метод предпочтительнее выполнения всех тестов набора тогда и только тогда, когда стоимость анализа меньше, чем стоимость выполнения невыбранных тестов. Для некоторых систем, критичных с точки зрения безопасности, стоимость пропуска ошибки может быть настолько высока, что небезопасные методы выборочного регрессионного тестирования использовать нельзя.
Примером безопасного метода может служить метод, который выбирает из T каждый тест, выполняющий, по крайней мере, один оператор, добавленный или измененный в P' или удаленный из P. Применение этого метода для регрессионного тестирования функции решения квадратного уравнения потребует построения матрицы покрытия, пример которой приведен в таблице на Рис. 12.1. Следует отметить, что матрица покрытия соответствует исходной версии программы, поскольку аналогичная информация для новой версии программы пока не собрана. Звездочка в ячейке таблицы означает, что соответствующий тест покрывает определенную строку кода; если тест не покрывает строку кода, ячейка оставлена пустой. Строки, измененные по отношению к исходной версии, выделены цветом. Легко заметить, что в соответствии с требованиями предложенного безопасного метода для повторного выполнения должны быть отобраны тесты 1, 2 и 5.
Методы минимизации
Процедура минимизации набора тестов ставит целью отбор минимального (в терминах количества тестов) подмножества T, необходимого для покрытия каждого элемента программы, зависящего от изменений. Для проверки корректности программы используются только тесты из минимального подмножества.

Рис. 12.1. Матрица покрытия тестируемого кода
Обоснование применения методов минимизации состоит в следующем:
- Корреляция между эффективностью обнаружения ошибок и покрытием кода выше, чем между эффективностью обнаружения ошибок и размером множества тестов. Неэффективное тестирование, например многочасовое выполнение тестов, не увеличивающих покрытие кода, может привести к ошибочному заключению о корректности программы.
- Независимо от способа порождения исходного набора тестов, его минимальные подмножества имеют преимущество в размере и эффективности, так как состоят из меньшего количества тестов, не ослабляя при этом способности к обнаружению ошибок или снижая ее незначительно.
- Вообще говоря, сокращенный набор тестов, отобранный при минимизации, может обнаруживать ошибки, не обнаруживаемые сокращенным набором того же размера, выбранным случайным или каким-либо другим способом. Такое преимущество минимизации перед случайными методами в эффективности является закономерным. Однако из всех детерминированных методов минимизация приводит к созданию наименее эффективных наборов тестов, хотя и самых маленьких. В частности, безопасные методы эффективнее методов минимизации, хотя и намного дороже.
Хотя минимальные наборы тестов могут обеспечивать структурное покрытие измененного кода, зачастую они не являются безопасными, поскольку очевидно, что некоторые тесты, потенциально способные обнаруживать ошибки, могут остаться за чертой отбора.
Набор функциональных тестов обычно не обладает избыточностью в том смысле, что никакие два теста не покрывают одни и те же функциональные требования. Если тесты исходно создавались по критерию структурного покрытия, минимизация приносит плоды, но когда мы имеем дело с функциональными тестами, предпочтительнее не отбрасывать тесты, потенциально способные обнаруживать ошибки. В существующей практике тестирования инженеры предпочитают не заниматься минимизацией набора тестов.
Многие критерии покрытия кода фактически не требуют выбора минимального множества тестов. В некотором смысле, о безопасных стратегиях и стратегиях минимизации можно думать как о находящихся на двух полюсах множества стратегий. На практике, использование "почти минимальных" наборов тестов может быть удовлетворительным. Стремление к сокращению объема набора тестов основано на интуитивном предположении, что неоднократное повторное выполнение кода в ходе модульного тестирования "расточительно". Однако усилия, требуемые для минимизации набора тестов, могут быть существенны, и, следовательно, могут не оправдывать затрат. Отметим, что большинство стратегий выборочного регрессионного тестирования, описанных в литературе, в общем-то, не зависит от критерия покрытия, возможно, использовавшегося при создании исходного набора тестов. Инженеры, занимающиеся регрессионным тестированием, часто не имеют информации о том, как разрабатывался исходный набор тестов.
Обнаружение ошибок важно для приложений, где стоимость выполнения тестов очень высока, в то время как стоимость пропуска ошибок считается незначительной. В этих условиях использование методов минимизации целесообразно, поскольку они связаны с отбором небольшого количества тестов. Примером применения методов минимизации служит метод, выбирающий из T не менее одного теста для каждого оператора программы, добавленного или измененного при создании P'. В таблице на Рис. 12.1 для случая регрессионного тестирования функции Equation данный метод ограничится отбором одного теста - теста 2, так как этот тест покрывает обе измененные строки.
Методы, основанные на покрытии кода
Значение методов, основанных на покрытии кода, состоит в том, что они гарантируют сохранение выбранным набором тестов требуемой степени покрытия элементов P' относительно некоторого критерия структурного покрытия C, использовавшегося при создании первоначального набора тестов. Это не означает, что если атрибут программы, определенный C, покрывается первоначальным множеством тестов, он будет также покрыт и выбранным множеством; гарантируется только сохранение процента покрываемого кода. Методы, основанные на покрытии, уменьшают разброс по покрытию, требуя отбора тестов, активирующих труднодоступный код, и исключения тестов, которые только дублируют покрытие. Поскольку на практике критерии покрытия кода обычно применяются для отбора единственного теста для каждого покрываемого элемента, подходы, основанные на покрытии кода, можно рассматривать как специфический вид методов минимизации.
Разновидностью методов, основанных на покрытии кода, являются методы, которые базируются на покрытии потока данных. Эти методы эффективнее методов минимизации и почти столь же эффективны, как безопасные методы. В то же время, они могут требовать, по крайней мере, такого же времени на анализ, как и наиболее эффективные безопасные методы, и, следовательно, могут обходиться дороже безопасных методов и намного дороже других методов минимизации. Они имеют тенденцию к включению избыточных тестов в набор регрессионных тестов для покрытия зависящих от изменений пар определения-использования, что, в некоторых случаях, ведет к большому числу отобранных тестов. Этот факт зафиксирован экспериментально.
Методы, основанные на использовании потока данных, могут быть полезны и для других задач регрессионного тестирования, кроме отбора тестов, например, для нахождения элементов P, недостаточно тестируемых T'.
Метод стопроцентного покрытия измененного кода аналогичен методу минимизации. Так, для примера таблицы c Рис. 12.1 существует 4 способа отобрать 2 теста в соответствии с этим критерием. Одного теста недостаточно.
Результаты сравнения методов выборочного регрессионного тестирования приведены в Табл. 12.1.
| От 0% до 100% | 100% | < 100% | < 100% |
| Настраивается | Большой | Небольшой | Зависит от параметров метода |
| Пренебрежимо мало | Значительное | Значительное | Значительное |
| Отсутствие средства поддержки регрессионного тестирования | Высокие требования по качеству | Стоимость пропуска ошибки невелика | Набор исходных тестов создается по критерию покрытия |

Случайные методы
Когда из-за ограничений по времени использование метода повторного прогона всех тестов невозможно, а программные средства отбора тестов недоступны, инженеры, ответственные за тестирование, могут выбирать тесты случайным образом или на основании "догадок", то есть предположительного соотнесения тестов с функциональными возможностями на основании предшествующих знаний или опыта. Например, если известно, что некоторые тесты задействуют особенно важные функциональные возможности или обнаруживали ошибки ранее, их было бы неплохо использовать также и для тестирования измененной программы. Один простой метод такого рода предусматривает случайный отбор предопределенного процента тестов из T. Подобные случайные методы принято обозначать random(x), где x - процент выбираемых тестов.
Случайные методы оказываются на удивление дешевыми и эффективными. Случайно выбранные входные данные могут давать больший разброс по покрытию кода, чем входные данные, которые используются в наборах тестов, основанных на покрытии, в одних случаях дублируя покрытие, а в других не обеспечивая его. При небольших интервалах тестирования их эффективность может быть как очень высокой, так и очень низкой. Это приводит и к большему разбросу статистики отбора тестов для таких наборов. Однако при увеличении интервала тестирования этот разброс становится значительно меньше, и средняя эффективность случайных методов приближается к эффективности метода повторного прогона всех тестов с небольшими отклонениями для разных попыток. Таким образом, в последнем случае пользователь случайных методов может быть более уверен в их эффективности. Вообще, детерминированные методы эффективнее случайных методов, но намного дороже, поскольку выборочные стратегии требуют большого количества времени и ресурсов при отборе тестов.
Если изменения в новой версии затрагивают код, выполняемый относительно часто, при случайных входных данных измененный код может в среднем активироваться даже чаще, чем при выполнении тестов, основанных на покрытии кода.
Это приведет к увеличению метрики количества отобранных тестов для случайных наборов. Наоборот, относительно редко выполняемый измененный код активируется случайными тестами реже, и соответствующая метрика снижается. При уменьшении мощности множества отобранных тестов падает эффективность обнаружения ошибок.
Когда выбранное подмножество, хотя и совершенное с точки зрения полноты и точности, все еще слишком дорого для регрессионного тестирования, особенно важна гибкость при отборе тестов. Какие дополнительные процедуры можно применить для дальнейшего уменьшения числа выбранных тестов? Одно из возможных решений - случайное исключение тестов. Однако, поскольку такое решение допускает произвольное удаление тестов, активирующих изменения в коде, существует высокий риск исключения всех тестов, обнаруживающих ошибку в этом коде. Тем не менее, если стоимость пропуска ошибок незначительна, а интервал тестирования велик, целесообразным будет использование случайного метода с небольшим процентом выбираемых тестов (25-30%), например, random(25).
Вернемся к примеру регрессионного тестирования функции решения квадратного уравнения. Случайный метод, такой, как random(40), может отобрать для повторного выполнения любые 2 теста из 5. Например, если будут выбраны тесты 4 и 5, изменения формата вывода на экран не будут протестированы вовсе, что вряд ли может устроить разработчика.
При использовании другого случайного метода - метода экспертных оценок - в данном случае наиболее вероятен выбор всех тестов, так как затраты на прогон невелики. Однако при регрессионном тестировании больших программных систем, когда повторный прогон всех тестов неприемлем, эксперт вынужден отсеивать некоторые тесты, что также может приводить к тому, что часть изменений не будет протестирована полностью.
Интеграционное регрессионное тестирование
Целесообразность отбора тестов
Поскольку в общем случае оптимальный отбор тестов (то есть выбор в точности тех тестов, которые обнаруживают ошибку) невозможен, соотношение между затратами на применение методов выборочного регрессионного тестирования и выигрышем от их использования является основным вопросом практического применения выборочного регрессионного тестирования. На основании оценки этого соотношения делается вывод о целесообразности отбора тестов.
Эффективное регрессионное тестирование представляет собой компромисс между качеством тестируемой программы и затратами на тестирование. Чем больше регрессионных тестов, тем полнее проверка правильности программы. Однако большее количество выполняемых тестов обычно означает увеличение финансовых затрат и времени на тестирование, что на практике не всегда приемлемо. Выполнение меньшего количества регрессионных тестов может оказаться дешевле, но не позволяет гарантировать сохранение качества.
Когда отдельные модули невелики и несложны, а связанные с ними наборы тестов также небольшие, простой повторный запуск всех тестов достаточно эффективен. При интеграционном тестировании это менее вероятно. В то время как тесты для отдельных модулей могут быть небольшими, тесты для групп модулей и подсистем достаточно велики, что создает предпосылки для уменьшения издержек тестирования. С другой стороны, с ростом размера приложений стоимость применения выборочной стратегии повторного тестирования может возрасти до неприемлемой величины. Затраты на необходимый для отбора анализ могут перевешивать экономию от прогона сокращенного набора тестов и анализа результатов прогона. Однако в области тестирования достаточно больших программ положительный баланс затрат и выгод вполне достижим.
Модель затрат и выгод при использовании выборочных стратегий регрессионного тестирования должна учитывать прямые и косвенные затраты. Прямые затраты включают отбор и выполнение тестов и анализ результатов. Косвенные затраты включают затраты на управление, сопровождение баз данных и разработку программных средств.
Выгоды – это затраты, которых удалось избежать, не выполняя часть тестов. Чтобы метод выборочного регрессионного тестирования был эффективнее метода повторного прогона всех тестов, стоимость анализа при отборе подмножества тестов вкупе со стоимостью их выполнения и проверки результатов должна быть меньше, чем стоимость выполнения и проверки результатов исходного набора тестов.
Пусть T' – подмножество T, отобранное некоторой стратегией выборочного регрессионного тестирования М для программы P, |T'| - обозначает мощность T', s – средняя стоимость отбора одного теста в результате применения М к P для создания T', а r – средняя стоимость выполнения одного теста из T на P и проверки его результата. Тогда для того, чтобы выборочное регрессионное тестирование было целесообразным, требуется выполнение неравенства:
s|T'| < r(|T| - |T'|)Применяя вышеупомянутую модель стоимости с целью анализа затрат, полезно условно разделять регрессионное тестирование на две фазы – предварительную и критическую. Предварительная фаза регрессионного тестирования начинается после выпуска очередной версии программного продукта; во время этой фазы разработчики расширяют функциональность программы и исправляют ошибки, готовясь к выпуску следующей версии. Одновременно тестировщики могут планировать будущее тестирование или выполнять задачи, требующие наличия только предыдущей версии программы, такие как сбор тестовых траекторий и анализ покрытия. Как только в программу внесены исправления, начинается критическая фаза регрессионного тестирования. В течение этой фазы регрессионное тестирование новой версии программы является доминирующим процессом, время которого обычно ограничено моментом поставки заказчику. Именно на критической фазе регрессионного тестирования наиболее важна минимизация затрат. При использовании выборочного метода регрессионного тестирования важно использовать факт наличия этих двух фаз, уделяя как можно больше внимания выполнению задач, связанных с анализом, в течение предварительной фазы, чтобы на критической фазе заниматься только прогоном тестов и уменьшить вероятность срыва сроков поставки.
Тем не менее, важно понимать, что до внесения последнего изменения в код анализ может быть выполнен только частично.
Если не учитывать не очень больших затрат на анализ при использовании детерминированных методов, решение о применении конкретного метода отбора тестов будет зависеть от отношения стоимости выполнения большего количества тестов к цене пропуска ошибки, что зависит от множества факторов, специфических для каждого конкретного случая. При отсутствии ошибок сбережения пропорциональны уменьшению размера набора тестов и могут быть измерены в терминах процента выбранных тестов, |T'| / |T|.
Модели стоимости могут использоваться как при выборе наилучшей, так и для оценки пригодности конкретной стратегии. При анализе учитываются такие факторы, как размер программы (в строках кода), мощность множества регрессионных тестов и количество покрываемых элементов, задействованных исходным множеством тестов.
Общий метод исследования проблемы целесообразности отбора тестов состоит в нахождении или создании исходной и измененной версий некоторой системы и соответствующего набора тестов. В этих условиях применяется методика отбора тестов, и размер и эффективность выбранного набора тестов сравнивается с размером и эффективностью первоначального набора тестов. Результаты показывают, что применение методов отбора регрессионных тестов, в том числе и безопасных, не всегда целесообразно, поскольку затраты и выгоды от их использования изменяются в широком диапазоне в зависимости от многих факторов. На практике наборы, основанные на покрытии, обеспечивают лучшие результаты отбора тестов.
Разумеется, отношение покрытия – не единственный фактор, который может отразиться на целесообразности применения выборочного регрессионного тестирования. Для некоторых приложений создание условий для тестирования (в том числе компиляция и загрузка модулей и ввод данных) может обходиться намного дороже, чем вычислительные ресурсы для непосредственного исполнения тестируемой системы. Например, в телекоммуникационной промышленности стоимость создания тестовой лаборатории для моделирования реальной сети связи может достигать нескольких миллионов долларов.
Подсчет порога целесообразности помогает определить, может ли отбор тестов вообще быть целесообразен для данного программного изделия и набора тестов. Однако даже в случаях, когда значение порога целесообразности указывает, что отбор тестов может быть целесообразен, он не обязательно будет таковым; результат зависит от параметров набора тестов, таких как размер набора, характеристики покрытия кода, уровень подробности и время выполнения тестов, а также от местоположения изменений. Существенно повлиять на общую оценку могут затраты на оплату труда тестового персонала, доступность свободного машинного времени для регрессионного тестирования, доступность стенда, на котором развернуто программное обеспечение приложения и т.п. Отметим, что стоимость прогона тестов связана не столько с размером программы, сколько с ограничениями на допустимое время прогона.
В некоторых случаях, когда число тестов, отброшенных выборочным методом регрессионного тестирования незначительно, но его применение тем не менее заслуживает внимания. Дело в том, что любое сокращение высокозатратного времени использования тестовой лаборатории особенно важно, а для отбора тестов используются другие ресурсы. Подобные обстоятельства необходимо включать в оценку стоимости анализа путем учета не только стоимости эксплуатации ресурса, но и таких факторов как время суток, день недели, время, оставшееся до выпуска очередной версии продукта и т.п. В этом случае модель стоимости должна соблюдать баланс между высокой стоимостью прогона тестов в тестовой лаборатории и относительно небольшой стоимостью проведения анализа на незанятых компьютерах.
Для некоторых программ и наборов тестов выборочное тестирование неэффективно, так как порог целесообразности превышает число тестов в наборе. В таких случаях методы отбора тестов, независимо от того, насколько успешно они уменьшают число тестов, требующих повторного выполнения, не могут давать экономию. Этот результат отражает тот факт, что целесообразность отбора зависит как от стоимости анализа, так и от стоимости выполнения тестов.Возможность достижения экономии при отборе регрессионных тестов для конкретной системы программного обеспечения и конкретного набора тестов должна оцениваться комплексно с учетом всех влияющих на решение факторов.
Стоит заметить, что целесообразность применения выборочного метода регрессионного тестирования нельзя воспринимать как нечто само собой разумеющееся. Следует очень осторожно подходить к оценке целесообразности отбора повторно прогоняемых тестов. В ряде случаев, когда или получаемое число остаточных тестов близко к первоначальному их количеству, или накладные расходы на повторное тестирование незначительны, выгоднее прогонять заново все тесты, особенно если прогон тестов полностью автоматизирован.
Функции предсказания целесообразности
На практике не существует способа в точности предсказать, сколько тестов будет выбрано при минимизации (или по результатам применения любой другой методики отбора тестов). Когда количество тестов, отсеянных по результатам отбора, незначительно, ресурсы, потраченные на отбор тестов, пропадают впустую. В таких случаях говорят, что выборочное регрессионное тестирование нецелесообразно. Чтобы выяснить, заслуживает ли внимания попытка применения выборочного метода регрессионного тестирования, необходимо использовать прогнозирующую функцию.
Прогнозирующие функции основаны на покрытии кода. Для их вычисления используется информация о доле тестов, активирующих покрываемые сущности – операторы, ветви или функции, – что позволяет предсказать количество тестов, которые будут выбраны при изменении этих сущностей. Существует как минимум одна прогнозирующая функция, которая может быть использована для предсказания целесообразности применения безопасной стратегии выборочного регрессионного тестирования.
Пусть P – тестируемая система, S – ее спецификация, T – набор регрессионных тестов для P, а |T| означает число отдельных тестов в T. Пусть М – выборочный метод регрессионного тестирования, используемый для отбора подмножества T при тестировании измененной версии P; М может зависеть от P, S, T, информации об исполнении T на P и других факторов. Через E обозначим набор рассматриваемых М сущностей тестируемой системы. Предполагается, что T и E непустые, и что каждый синтаксический элемент P принадлежит, по крайней мере, одной сущности из E. Отношение coversM(t, E) определяется как отношение покрытия, достигаемого методом М для P. Это отношение определено над T?E и справедливо тогда и только тогда, когда выполнение теста t на P приводит к выполнению сущности e как минимум один раз. Значение термина "выполнение" определено для всех типов сущностей P. Например, если e – функция или модуль P, e выполняется при вызове этой функции или модуля. Если e – простой оператор, условный оператор, пара определения-использования или другой вид элемента пути в графе выполнения P, e выполняется при выполнении этого элемента пути.
Если e – переменная P, e выполняется при чтении или записи этой переменной. Если e – тип P, e выполняется при выполнении любой переменной типа e. Если e – макроопределение P, e выполняется при выполнении расширения этого макроопределения. Если e – сектор P, e выполняется при выполнении всех составляющих его операторов. Соответствующие значения термина "выполнение" могут быть определены по аналогии для других типов сущностей P.
Для данной тестируемой системы P, набора регрессионных тестов T и выборочного метода регрессионного тестирования М можно предсказать, стоит ли задействовать М для регрессионного тестирования будущих версий P, используя информацию об отношении покрытия coversM, достигаемого при использовании М для T и P. Прогноз основан на метрике стоимости, соответствующей P и T. Относительно издержек принимаются некоторые упрощающие предположения.
Пусть EC обозначает множество покрытых сущностей:
EC = {e



Ci, j = 1, если coversМ(i, j) Ci, j = 0, иначеСтепень накопленного покрытия, обеспечиваемого T, то есть общее число единиц в матрице C, обозначается CC:
|T| |E| CC = ? ? Ci,j i=1 j=1Отметим, что если ограничиться включением в C только столбцов, соответствующих покрытым сущностям EC, накопленное покрытие CC останется неизменным. В частности, для всех непокрытых сущностей u Ci,u равно нулю для всех тестов i (так как coversM(i, u) ложно для всех таких случаев). Следовательно, ограничение на EC при вычислении суммы, определяющей CC, приводит только к исключению слагаемых, равных нулю.
Пусть TM – подмножество T, выбранное М для P, и пусть |TM| обозначает его мощность, тогда TM = {t

М целесообразно использовать в качестве метода отбора тестов тогда и только тогда, когда:
sM|TM| < r(|T| - |TM|), то есть стоимость анализа, необходимого для отбора TM, должна быть меньше стоимости прогона невыбранных тестов, T

Оценка ожидаемого числа тестов, требующих повторного запуска, обозначается NM и вычисляется следующим образом:
Nm = CC/|E|Использование этой прогнозирующей функции предполагается только в случаях, когда цель выборочной стратегии регрессионного тестирования состоит в повторном выполнении всех тестов, затронутых изменениями, то есть используется безопасный метод отбора тестов. Несколько усовершенствованный вариант оценки NM, использующий в качестве пространства сущностей EC вместо E:
Ncm = CC/|Ec| Прогнозирующая функция для доли набора тестов, требующей повторного выполнения, то есть для |TM| / |T|, обозначается



- Целесообразность применения выборочного метода регрессионного тестированияи, как следствие, наша способность к предсказанию целесообразности, непосредственно зависит от доли тестового набора, выбираемой для выполнения методом регрессионного тестирования.
- Эта доля в свою очередь непосредственно зависит от отношения покрытия.
Существуют факторы, влияющие на целесообразность отбора тестов, но не учитываемые прогнозирующей функцией.
Один из подходов улучшения качества прогноза состоит в использовании информации об истории изменений программы, которую зачастую можно получить из системы управления конфигурацией.
Например, в рис. 12.1 прогнозирующая функция может быть подсчитана как отношение общего количества звездочек в таблице к количеству строк таблицы, т.е. числу покрываемых сущностей. Эта величина составляет 42/11=3.8, т.е. безопасный метод будет отбирать в среднем около 4 тестов. Сведения о методике предсказания суммированы в Табл. 13.3.
| Время работы метода отбора в случае, если выборочное тестирование целесообразно | Увеличивается незначительно |
| Время работы метода отбора в случае, если выборочное тестирование нецелесообразно | Уменьшается до пренебрежимо малой величины |
| Снижение точности предсказания от версии к версии | Зависит от объема изменений |
| Результаты применения методики на практике | Положительные (ошибка в 0.8%) |
Интеграционное регрессионное тестирование
С появлением новых направлений в разработке программного обеспечения (например, объектно-ориентированного программирования), поощряющих использование большого количества маленьких процедур, повышается важность обработки межмодульного влияния изменений кода для методик уменьшения стоимости регрессионного тестирования. Для решения этой задачи необходимо рассматривать зависимости по глобальным переменным, когда переменной в одной или нескольких процедурах присваивается значение, которое затем используется во многих других процедурах. Такую зависимость можно рассматривать как зависимость между процедурами по потоку данных. Также возможны межмодульные зависимости по ресурсам, например по памяти, когда ресурс разделяется между несколькими процедурами. Отметим, что при системном регрессионном тестировании зависимости такого рода можно игнорировать.
Если изменение спецификации требований затрагивает глобальную переменную, могут потребоваться новые модульные тесты. В противном случае, повторному выполнению подлежат только модульные тесты, затрагивающие как измененный код, так и операторы, содержащие ссылку на глобальную переменную.
Брандмауэр можно определить как подмножество графа вызовов, содержащее измененные и зависящие от изменений процедуры и интерфейсы. Методы отбора тестов, использующие брандмауэр, требуют повторного интеграционного тестирования только тех процедур и интерфейсов, которые непосредственно вызывают или вызываются из измененных процедур.
Методы упорядочения
Методы упорядочения позволяют инженерам-тестировщикам распределить тесты так, что тесты с более высоким приоритетом исполняются раньше, чем тесты с более низким приоритетом, чтобы затем ограничиться выбором первых n тестов для повторного исполнения. Это особенно важно для случаев, когда тестировщики, могут позволить себе повторное выполнение только небольшого количества регрессионных тестов.
Одной из проблем упорядочения тестов является отсутствие представлений о том, скольких тестов в конкретном проекте достаточно для отбора. В отличие от подхода минимизации, использующего все тесты минимального набора, оптимальное число тестов в упорядоченном наборе неизвестно. Возникает проблема баланса между тем, что необходимо делать в ходе регрессионного тестирования, и тем, что мы можем себе позволить. В итоге количество запускаемых тестов определяется ограничениями по времени и бюджету и порядком тестов в наборе. С учетом этих факторов следует запускать повторно как можно больше тестов, начиная с верхней строки списка упорядоченных тестов.
Возможное преимущество упорядочения тестов состоит в том, что сложность соответствующего алгоритма, O(n2) в наихудшем случае, меньше, чем сложность алгоритма минимизации, который в некоторых случаях может требовать экспоненциального времени выполнения.
Проблему упорядочения тестов можно сформулировать следующим образом:
- Дано: T - набор тестов, PT - набор перестановок T, f - функция из PT на множество вещественных чисел.
- Найти: набор T'PT такой, что: (
T'') (T''
PT)(T''
T') [f(T')
f(T'')]
Упорядочение может преследовать различные цели:
- Увеличение частоты обнаружения ошибок наборами тестов, то есть увеличение вероятности обнаружить ошибку раньше при выполнении регрессионных тестов из этих наборов.
- Ускорение процесса покрытия кода тестируемой системы и достижение требуемой степени покрытия кода на более ранних этапах процесса тестирования.
- Быстрейший рост вероятности того, что тестируемая система надежна.
- Увеличение вероятности обнаружения ошибок, связанных с конкретными изменениями кода, на ранних этапах процесса тестирования и т.п.
Обоснование использования упорядочивающего метода состоит в том, что упорядоченный набор тестов имеет большую вероятность достижения цели, чем тесты, расположенные по какому-либо другому правилу или в случайном порядке.
Различают два типа упорядочения тестов: общее и в зависимости от версии. Общее упорядочение тестов при данных программе P и наборе тестов T подразумевает нахождение порядка тестов T, который окажется полезным для тестирования нескольких последовательных измененных версий. Считается, что для этих версий итоговый упорядоченный набор тестов позволяет в среднем быстрее достигнуть цели, ради которой производилось упорядочение, чем исходный набор тестов. Однако имеется значительный объем статистических свидетельств в пользу наличия связи между частотой обнаружения ошибок и конкретной версией тестируемой программы: разные версии программы предоставляют различные возможности по упорядочению тестов. При регрессионном тестировании мы имеем дело с конкретной версией программного продукта и хотим упорядочивать тесты наиболее эффективно по отношению именно к этой версии.
Например, упорядочивать тесты можно по количеству покрываемых ими изменений кода. При безопасном отборе тестов из рис. 12.1 будут выбраны тесты 1, 2 и 5, из которых наиболее приоритетным является тест 2, так как он затрагивает оба изменения, тогда как тесты 1 и 5 – только одно.
Сведения о методике упорядочения тестов суммированы в Табл. 13.3.
| Время работы метода отбора | Увеличивается незначительно |
| Частота обнаружения ошибок | Увеличивается |
| Скорость покрытия кода | Увеличивается |
| Результаты применения методики на практике | Положительные |
Порождение новых тестов
Порождение новых тестов при структурном регрессионном тестировании обычно обусловлено недостаточным уровнем покрытия. Новые тесты разрабатываются так, чтобы задействовать еще не покрытые участки исходного кода. Процесс прекращается, когда уровень покрытия кода достигает требуемой величины (например, 80%). Разработка новых тестов при функциональном регрессионном тестировании является менее тривиальной задачей и обычно связана с вводом новых требований либо с желанием проверить некоторые сценарии работы системы дополнительно.
Основой большинства программных продуктов для управляющих применений, находящихся в промышленном использовании, является цикл обработки событий. Сценарий работы с системой, построенной по такой архитектуре, состоит из последовательности транзакций, управление после обработки каждой транзакции вновь передается циклу обработки событий. Выполнение транзакции приводит к изменению состояния программы; в результате некоторых транзакций происходит выход из цикла и завершение работы программы. Тесты для таких программ представляют собой последовательность транзакций.
Развитие программного продукта от версии к версии влечет за собой появление новых состояний. Поскольку большинство тестов легко может быть расширено путем добавления дополнительных транзакций в список, новые тесты можно создавать путем суперпозиции уже имеющихся, с учетом информации об изменении состояния тестируемой системы в результате прогона теста. Этот подход позволяет указать, какого рода новые тесты с наибольшей вероятностью обнаружат ошибки.
Обозначим тестируемую программу P, а множество ее тестов T = {t1, t2, … , tn}. Будем считать, что состояние тестируемой программы s определяется совокупностью значений некоторого подмножества глобальных и локальных переменных. При создании новых тестов будем рассматривать состояния программы перед запуском теста (s0) и после его окончания (sj). Информацию об этих состояниях необходимо собирать для каждого теста по результатам запуска на предыдущей версии продукта. Методика порождения новых тестов на основе анализа "подозрительных" состояний сводится к описанной ниже последовательности действий.
- Вычисление списка глобальных и локальных переменных, определяющих состояние программы s.
- Сбор информации (на основе анализа профиля программы, полученного на предыдущей версии продукта i-1, для каждого существующего теста tj) о состояниях программы перед запуском теста и после его окончания (т.е. s0 и sj). Множество таких состояний обозначается Si - 1: Si – 1 = s0{sj |
j}
- Выполнение на текущей версии продукта i новых и выбранных регрессионных тестов из множества T’ . По аналогии с Si–1 вычисляется множество Si, которое сохраняется под управлением системы контроля версий.
- Оценка "подозрительных" с точки зрения наличия ошибок множества новых по сравнению с предыдущими версиями состояний Ni в соответствии со следующей формулой: Ni = Si \ Si - 1
- Анализ состояний множества Ni, в которых дальнейшая работа продукта невозможна в соответствии со спецификацией. Предмет анализа - определить создаются ли эти состояния в результате выполнения тестов, проверяющих нештатные режимы работы продукта, или каких-либо других тестов. В последнем случае фиксируется ошибка.
- Исключение нештатных состояний из множества Ni.
- Переход к шагу 10, если новых состояний, допускающих продолжение выполнения программы, не обнаружено, т.е. Ni=.

- Для каждого состояния множества Ni вычисление вектора отличия от исходного состояния s0, т.е. множества переменных, измененных по сравнению с s0.
- Модификация множества измененных строк исходного кода P на основе информации об измененных переменных и использование какой-либо методики отбора тестов для выборочного регрессионного тестирования.
- Повторное выполнение шагов 3-9 до достижения состояния Ni=либо до истечения времени, отведенного на регрессионное тестирование. Использование методов разбиения на классы эквивалентности для досрочного принятия решения о прекращении цикла тестирования, если ни один из тестов, созданных на очередном этапе, не принадлежит к новому классу эквивалентности.

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

Регрессионное тестирование объектно-ориентированных программ
Объектно-ориентированный подход стимулирует новые приложения методик выборочного повторного тестирования. Действительно, при изменении класса необходимо обнаружить в наборе тестов класса только тесты, требующие повторного выполнения. Точно так же при порождении нового класса из существующего необходимо определить тесты из множества тестов базового класса, требующие повторного выполнения на классе-потомке. Хотя благодаря инкапсуляции вероятность ошибочного взаимодействия объектно-ориентированных модулей кода уменьшается, тем не менее, возможно, что тестирование прикладных программ выявит ошибки в методах, не найденные при модульном тестировании методов. В этом случае необходимо рассмотреть все прикладные программы, использующие измененный класс, чтобы продемонстрировать, что все существующие тесты, способные обнаруживать ошибки в измененных классах, были запущены повторно и было выбрано безопасное множество тестов. При повторном тестировании прикладных программ, классов или их наследников применение методик выборочного повторного тестирования к существующим наборам тестов может принести немалую пользу.
В объектно-ориентированной программе вызов метода во время выполнения может быть сопоставлен любому из ряда методов. Для заданного вызова мы не всегда можем статически определить метод, с которым он будет связан. Выборочные методы повторного тестирования, которые полагаются на статический анализ, должны обеспечивать механизмы для разрешения этой неопределенности.
Уменьшение объема тестируемой программы
Еще один путь сокращения затрат на регрессионное тестирование состоит в том, чтобы вместо повторного тестирования (большой) измененной программы с использованием соответственно большого числа тестов доказать, что измененная программа адекватно тестируется с помощью выполнения некоторого (меньшего) числа тестов на остаточной программе. Остаточная программа создается путем использования графа зависимости системы вместо графа потока управления, что позволяет исключить ненужные зависимости между компонентами в пределах одного пути графа потока управления. Так, корректировка какого-либо оператора в идеале должна приводить к необходимости тестировать остаточную программу, состоящую из всех операторов исходной программы, способных повлиять на этот оператор или оказаться в сфере его влияния. Для получения остаточной программы необходимо знать место корректировки (в терминах операторов), а также информационные и управляющие связи в программе. Этот подход работает лучше всего для малых и средних изменений больших программ, где высокая стоимость регрессионного тестирования может заставить вообще отказаться от его проведения. Наличие дешевого метода остаточных программ, обеспечивающего такую же степень покрытия кода, делает регрессионное тестирование успешным даже в таких случаях.
Метод остаточных программ имеет ряд ограничений. В частности, он не работает при переносе программы на машину с другим процессором или объемом памяти. Более того, он может давать неверные результаты и на той же самой машине, если поведение программы зависит от адреса ее начальной загрузки, или если для остаточной программы требуется меньше памяти, чем для измененной, и, соответственно, на остаточной программе проходит тест, который для измененной программы вызвал бы ошибку нехватки памяти. Исследования метода на программах небольшого объема показали, что выполнение меньшего количества тестов на остаточной программе не оправдывает затрат на отбор тестов и уменьшение объема программы. Однако для программ с большими наборами тестов это не так.
Для теста 1 рис. 12. 1 для функции Equation остаточная программа выглядит так, как показано в Табл. 13.1. Нумерация строк оставлена такой же, как в исходной программе. Таким образом, можно заметить, что были удалены строки 6 и 7, которые не затрагиваются тестом 1 в ходе его выполнения, а также строки 3 и 8, содержащие вычисление предикатов, которые в ходе выполнения теста всегда истинны. Запуск теста на полной измененной программе и на остаточной программе приводит к активизации одних и тех же операторов, поэтому выигрыша во времени получить не удается, однако за счет сокращения объема программы уменьшается время компиляции. Для нашего примера этот выигрыш незначителен и не оправдывает затрат на анализ, необходимый для уменьшения объема. Таким образом, рассмотренная технология рекомендуется к применению, прежде всего, в случаях, когда стоимость компиляции относительно высока.
| 1 | double Equation(int Print, float A, float B, float C, float& X1, float& X2) { |
| 2 | float D = B * B - 4.0 * A * C; |
| 4 | X1 = (-B + sqrt(D)) / 2.0 / A; |
| 5 | X2 = (-B - sqrt(D)) / 2.0 / A; |
| 9 | printf("Solution: %f, %f\n", X1, X2); |
| 10 | return D; |
| 11 | } |
| Время компиляции тестируемой программы | Уменьшается |
| Время выполнения тестируемой программы | Не изменяется |
| Время работы метода отбора | Увеличивается |
| Риск пропуска ошибок | Увеличивается |
| Результаты применения методики на практике | Отрицательные |
Методика регрессионного тестирования
Методика регрессионного тестирования
Методика предназначена для эффективного решения задачи выборочного повторного тестирования. Ее исходными данными являются: программа P и ее модифицированная версия P', критерий тестирования C, множество (набор) тестов T, ранее использовавшихся для тестирования P, информация о покрытии элементов P (M(P,C)) тестами из T. Необходимо реализовать эффективный способ, гарантирующий достаточную степень уверенности в правильности P', используя тесты из T.
Методика строится на основе сочетания процедур обычного и регрессионного тестирования
Рассмотрим процедуру обычного тестирования. В ней для получения информации о тестируемых объектах в ходе тестирования необходимо установить соответствие между покрываемыми элементами и тестами для их проверки. Соответственно, процедура тестирования должна включать приведенную ниже последовательность действий:
- Определить требуемые функциональные возможности программы с использованием, например, метода разбиения на классы эквивалентности.
- Создать тесты для требуемых функциональных возможностей.
- Выполнить тесты.
- В случае необходимости - создать и выполнить дополнительные тесты для покрытия оставшихся (еще не покрытых) структурных элементов (предварительно установив их соответствие функциональным требованиям).
- Создать базу данных тестов программы.
- Использование функции предсказания целесообразности. Если прогнозируемое количество выбранных тестов больше, чем порог целесообразности, провести повторный прогон всех тестов. В противном случае перейти к шагу 2.
- Идентификация изменений ?P в программе P' (и множества ?М измененных покрываемых элементов) и установление взаимно однозначного соответствия между покрываемыми элементами М(P, C) и М(P', C) в соответствии с изменениями: ?M = (M(P, C) \ M(P', C))(M(P', C) \ M(P, C))

- Выбор T'?T - подмножества исходных тестов, потенциально способных выявить связанные с изменениями ошибки в P', для повторного выполнения на P', с использованием результатов, полученных в пункте 2.
Это подмножество можно упорядочить, а также указать число тестов, выполнения которых достаточно для соответствия какому-либо критерию минимизации. Для безопасных методов отбора тестов множество T' удовлетворяет следующим ограничениям:tiT, ti
T'
P(ti)
P'(ti)
- Применение подмножества T' для регрессионного тестирования измененной программы P' с целью проверки результатов и установления факта корректности P' по отношению к T' (в соответствии с измененным техническим заданием), а также обновления информации о прохождении тестов из T' на P'.
- В случае необходимости - создание дополнительных тестов для дополнения набора регрессионных тестов. Это могут быть новые функциональные тесты, необходимые для тестирования изменений в техническом задании или новых функциональных возможностей измененной программы; новые структурные тесты для активизации оставшихся (непокрытых) структурных элементов (предварительно установив их соответствие проверяемым функциональным требованиям).
- Создание T'' - нового набора тестов для P', применение его для тестирования измененной программы, проверка результатов и установление факта корректности P' по отношению к T'', обновление информации о ходе исполнения теста и создание базы данных тестов измененной программы для хранения этой информации и выходных данных тестов. Удаление устаревших тестов. T'' формируется по следующему правилу:T'' = (TTновые) \ Tустаревшие

Система поддержки регрессионного тестирования
Структура системы поддержки регрессионного тестирования представлена на рис. 14.1. Исходный код обеих версий тестируемой программы хранится под управлением системы контроля версий. Для отбора тестов по методу покрытия точек использования неисполняемых определений средствами системы контроля версий создается файл различий, на основании которого вычисляется список добавленных, измененных и удаленных строк исходного кода, который является удобной формой представления множества ?P. Затем производится перебор этого списка. Если какая-либо строка списка представляет собой макроопределение, осуществляется поиск строк кода, содержащих использование этого макроопределения по всему тексту тестируемой программы; найденные строки присоединяются к множеству ?P. Расширенное множество ?P сопоставляется с результатами прогона тестов из множества T на предыдущей версии программы. Если в ходе выполнения какого-либо теста ti получала управление хотя бы одна строка, входящая в множество ?P, тест ti отбирается для повторного запуска.
При создании новых тестов по методу "подозрительных" состояний функция тестируемой программы, содержащая цикл обработки событий, дополняется операторами вывода значений глобальных и видимых локальных переменных. Запуск тестов из множества T' на профилированной версии программы позволяет получить список ее состояний. Этот список анализируется, и для каждого ранее не наблюдавшегося состояния вычисляется список переменных, изменившихся по сравнению с каким-либо известным состоянием. Множество ?P дополняется строками кода, где используются переменные из этого списка. Для каждого состояния указываются тесты, запуск которых необходим. Наконец, создается список рекомендованных новых тестов в форме, удобной для восприятия человеком.
Выходные данные каждой программы-обработчика доступны пользователю, что позволяет контролировать промежуточные результаты работы системы. К примеру, можно исключить из рассмотрения переменные, которые, хотя и изменяются в ходе выполнения программы, на ее состояние не влияют. Архитектура системы позволяет легко расширять функциональность; например, для поддержания какой-либо новой системы контроля версий достаточно создать один новый модуль объемом около 100 строк кода. Остальные модули можно использовать без изменений.
Типовой сценарий проведения регрессионного тестирования программ, написанных на языке C, с применением описанной выше системы состоит из следующих этапов:
- Вычисляется множество ?P строк исходного кода, добавленных, удаленных или измененных по сравнению с предыдущей версией.
- Множество ?P дополняется строками, непосредственно не изменявшимися, но содержащими ссылки на измененные макроопределения
- Вычисляется упорядоченное множество регрессионных тестов T', для которых ?iT' Ti, j - 1??P

, где Ti, j-1 - множество строк исходного кода продукта, получающих управление в ходе выполнения теста i на версии системы j-1. Тесты упорядочиваются по убыванию количества измененных строк в пути их исполнения.
- Вычисляется список глобальных и локальных переменных, определяющих состояние программы s. Исходный код тестируемой программы модифицируется так, что информация о состоянии s (значения глобальных, статических и локальных переменных) выводится во внешний файл перед запуском теста и после его окончания.
- Новые тесты и тесты из множества T' (регрессионные тесты) исполняются на текущей версии продукта j.
- Тесты, проверяющие нештатные режимы работы продукта, т.е. создающие состояния, в которых дальнейшая работа продукта невозможна в соответствии со спецификацией требований, исключаются из рассмотрения. Если известно, что ни один тест не приводит к возникновению нештатных состояний, данный этап может быть опущен
- Для каждого теста i вычисляется множество Tij.
- Обрабатываются результаты выполнения тестов, и создается множество Sj, состоящее из начального состояния s0 и всех наблюдавшихся конечных состояний. Вычисляется множество Nj= Sj\Sj-1 всех новых по сравнению с предыдущими версиями состояний, которое является "подозрительным" с точки зрения наличия ошибок, и вектора их отличий от исходного состояния s0, т.е. переменные, измененные по сравнению с s0.
- Множество измененных строк исходного кода ?P дополняется номерами строк, где используются заданные переменные
- Если Nj =, цикл работы завершается. Если Nj

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

Рис. 14.1. Структура системы поддержки регрессионного тестирования.
Если этап 6 выполняется автоматически, а этап 7 можно опустить, возможна полная автоматизация регрессионного тестирования.

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

Рис. 1.5. Как тестировать
Мы будем использовать оба подхода. При тестировании классов мы будем стремиться покрыть как спецификации классов, так и код их реализации. При тестировании взаимодействий будем покрывать спецификацию. При системном тестировании также будем стремиться покрыть спецификацию системы.
Какие компоненты надо тестировать?
Могут быть варианты, когда ничего не надо тестировать (поскольку все компоненты были протестированы ранее), а может потребоваться тестировать каждый компонент с точностью до строки кода. В объектно- ориентированном программировании базовым компонентом является класс. В этом случае область тестирования определяется классами. Область тестирования на уровне классов подлежит выбору (рис. 1.3). Классы, заимствованные из других проектов или взятые из библиотек, чаще всего в повторном тестировании не нуждаются. Существуют различные стратегии по выбору подмножества классов для тестирования.

Рис. 1.3. Что тестировать
В нашем случае будет производиться тестирование всех классов приложения.
Когда надо тестировать?
Компоненты можно тестировать на завершающем этапе, когда они будут интегрированы в единый выполняемый модуль. Частота тестирования определяется различными соображениями. Можно проводить тестирование каждый день, учитывая тот факт, что чем раньше выявлена проблема, тем легче и дешевле ее решение. Можно тестировать программный компонент по мере завершения его разработки (рис. 1.4). Частое тестирование компонентов несколько замедляет ранние этапы разработки, однако сопряженные с этим потери с лихвой восполняются за счет меньшего числа проблем на более поздних этапах разработки проекта, когда отдельные модули объединяются в более крупные компоненты системы.
В случае, когда компоненты системы не отличаются большой сложностью, можно сначала осуществлять интегрирование не подвергавшихся автономному тестированию компонентов, а затем тестировать объединенный код как единое целое. Такой подход полезен при тестировании компонентов, для которых реализация тестовых драйверов требует существенных усилий. Тестовый драйвер представляет собой программу, которая выполняет прогон тестовых случаев и сбор полученных при этом результатов.

Рис. 1.4. Когда тестировать
Студентам предлагается тестировать компоненты по мере их готовности.
Кто будет тестировать?
Разработчик - это роль, для которой характерны виды деятельности, ориентированные на создание программного продукта (ПП). Тестировщик - это роль, для которой характерны виды деятельности, ориентированные на обнаружение ошибок в ПП. Эта роль предусматривает выбор тестов, необходимых для конкретных целей, построение тестов, выполнение тестов и оценку результатов. Конкретный исполнитель проекта может выступать как в роли разработчика, так и в роли тестировщика. Момент начала тестирования в проекте можно регулировать (рис. 1.2).

Рис. 1.2. Кто тестирует
В рамках данного практикума студенту предназначена роль тестировщика.
Описание тестируемой системы и ее окружения
Практикум базируется на тестировании модели реальной системы управления автоматизированным комплексом хранения подшипников. Она обеспечивает прием подшипников на склад, сохранение характеристик поступивших подшипников в базе данных (БД), а при поступлении заявки на подшипники вместе с параметрами оси - подбор подходящих подшипников и их выдачу. У каждого из элементов комплекса (склада, терминала подшипника и терминала оси) существует программа низкоуровневого управления, реализованная в виде динамически подключаемой библиотеки (dll), принимающая на вход высокоуровневые команды, и преобразующая их в управляющие воздействия на данный элемент тестируемой системы. Таким образом, есть реальное окружение - аппаратура и dll, которые осуществляют связь с аппаратурой. Система вызывает следующие функции из dll для своих элементов (см. рис. 1.1):
GetStoreStat, GetStoreMessage, SendStoreCom (Store.dll) для склада.
GetAxlePar (Axle.dll) для терминала оси.
GetRollerPar (Bearing.dll) для терминала подшипника.

Рис. 1.1. Система и ее окружение
Поскольку для тестирования используется модель системы, в ее составе реальное окружение заменено на модельное, обеспечиваемое специальной библиотекой dll-функций окружения.
Тестируемая система реализована как многопоточное приложение. Многопоточность порождает недетерминированность поведения системы во времени. Поэтому при тестировании необходимо учитывать, что возможны различные варианты допустимых временных последовательностей событий системы. Кроме того, совсем не просто точно воспроизвести прогон конкретного теста, когда система содержит много параллельных потоков, так как планировщик операционной системы сам определяет порядок событий. Изменения, вносимые в программы, не связанные с тестируемой системой, могут повлиять на порядок, в котором будут выполняться (воспроизводиться) потоки событий тестируемой системы. Это может привести к тому, что после выявления и исправления дефекта при проведении повторного тестирования далеко не всегда удастся убедиться в том, что дефект действительно устранен, если ошибка не обнаружена во время прогона.
При системном тестировании мы рассматриваем систему как черный ящик. Тестовый случай (test case) представляет собой пару (входные данные, ожидаемый результат), в которой входные данные - это описание данных, подаваемых на вход нашей системы, а ожидаемый результат - это описание выходных данных, которые система должна предъявить в ответ на соответствующий ввод. Выполнение (прогон) тестового случая - это сеанс работы системы, в рамках которого на вход системы подаются наборы данных, предусмотренные спецификацией тестового случая, и фиксируются результаты их обработки, которые затем сравниваются с ожидаемыми результатами, указанными в тестовом случае. Если фактический результат отличается от ожидаемого, значит, обнаружен отказ, т.е. тестируемая система не прошла испытание на заданном тестовом случае. Если полученный результат совпадает с ожидаемым, значит, тестируемая система прошла испытание на заданном тестовом случае. Из тестовых случаев формируются тестовые наборы (test suits). Тестовые наборы организованы в определенном порядке, отражающем свойства тестовых случаев. Если система успешно справилась со всеми тестовыми случаями из набора, то она успешно прошла испытания на тестовом наборе.
Для нашей системы входными данными является состояние окружения ее компонентов:
Склад. Состояние склада будет характеризоваться следующими параметрами:
Статус склада (StoreStat).
Сообщение от склада о результатах выполнения команды (StoreMessage).
Сообщение от склада о результатах получения команды - статус команды (CommandStatus).
Терминал подшипника. Состояние терминала подшипника задается следующими параметрами:
Статус обмена с терминалом подшипника.
Характеристики (параметры) подшипника (RollerPar):
ФИО мастера, производившего измерения.
Название депо.
Номер рабочей смены.
Номер подшипника.
Номер группы подшипника.
Тип сепаратора подшипника.
Терминал оси. Состояние терминала оси задается следующими параметрами:
Статус обмена с терминалом оси.
Характеристики (параметры) оси (AxlePar):
ФИО мастера, производившего измерения.
Название депо.
Номер оси.
Сторона оси: правая или левая.
Посадочный диаметр задний.
Посадочный диаметр передний.
База данных (БД). В БД хранятся характеристики поступивших на склад подшипников. При выборе подходящего для оси подшипника система обращается за этой информацией к БД. Поэтому имеет смысл предварительно очистить БД или заполнить ее определенными данными.
В спецификации тестового случая должны быть заданы состояние окружения (входные данные) и ожидаемая последовательность событий в системе (ожидаемый результат). После прогона тестового случая мы получим реальную последовательность событий в системе (выходные данные) при заданном состоянии окружения. Сравнивая фактический результат и ожидаемый, можно сделать вывод о том, прошла ли тестируемая система испытание на заданном тестовом случае. В качестве ожидаемого результата будем использовать пошаговое описание случая использования (use case), так как оно определяет, как при заданном состоянии окружения система должна функционировать. Задавая ожидаемый результат, очень важно помнить о том, что при заданном состоянии окружения возможны различные варианты последовательности событий системы, которые все являются правильными.
В процессе работы последовательность событий (команд) системы, или история системы, записывается в журнал (log) системы. Вы можете использовать SystemLogAnimator (см. п.14 SysLog Animator Manual) для визуализации журнала системы. Выбирая различные log-файлы системы для визуализации, можно получить наглядное и достаточно полное представление о функционировании системы и о том, какие события и в каком порядке могут происходить в системе.
Планирование тестирования
Как было отмечено выше, процесс тестирования тесно связан с процессом разработки. Соответственно планирование тестирования тоже зависит от выбранной модели разработки. Однако вне зависимости от модели разработки при планировании тестирования необходимо ответить на пять вопросов, определяющих этот процесс:
Кто будет тестировать и на каких этапах?
Разработчики продукта, независимая группа тестировщиков или совместно?
Какие компоненты надо тестировать?
Будут ли подвергнуты тестированию все компоненты программного продукта или только компоненты, которые угрожают наибольшими потерями для всего проекта?
Когда надо тестировать?
Будет ли это непрерывный процесс, вид деятельности, выполняемый в специальных контрольных точках, или вид деятельности, выполняемый на завершающей стадии разработки?
Как надо тестировать?
Будет ли тестирование сосредоточено только на проверке того, что данный продукт должен выполнять, или также на том, как это реализовано?
В каком объеме тестировать?
Как определить, в достаточном ли объеме выполнено тестирование, или как распределить ограниченные ресурсы, выделенные под тестирование?
Процесс тестирования
Процесс тестирования находится в прямой зависимости от процесса разработки программного обеспечения, но при этом сильно отличается от него, поскольку преследует другие цели. Разработка ориентирована на построение программного продукта, тогда как тестирование отвечает на вопрос, соответствует ли разрабатываемый программный продукт требованиям, в которых зафиксирован первоначальный замысел изделия (т.е. то, что заказал заказчик).
Вместе оба процесса охватывают виды деятельности, необходимые для получения качественного продукта. Ошибки могут быть привнесены на каждой стадии разработки. Следовательно, каждому этапу разработки должен соответствовать этап тестирования. Отношения между этими процессами таковы, что если что-то разрабатывается, то оно подвергается тестированию, а результаты тестирования используются для определения, соответствует ли это "что-то" набору предъявляемых требований. Процесс тестирования возвращает выявленные им ошибки в процесс разработки. Процесс разработки передает процессу тестирования новые и исправленные проектные версии.
В каком объеме тестировать?
Различные уровни адекватного тестирования изображены на рис. 1.6, который охватывает случаи от отсутствия тестирования до исчерпывающего тестирования, когда выполняется прогон всех возможных тестовых случаев. Объем необходимого тестирования следует определять исходя из краткосрочных и долгосрочных целей проекта и в соответствии с особенностями разрабатываемого ПП. Покрытие - это мера полноты использования возможностей программного компонента тестовым набором.
Например, одна из мер - задействована ли каждая строка программного кода продукта хотя бы один раз при прогоне данного тестового набора. Другая мера - количество требований спецификации, проверенных данным тестовым набором. Если требования сформулированы в терминах случаев использования, то покрытие измеряется количеством случаев использования и числом сценариев, построенных для каждого случая использования.
Анализ рисков в процессе тестирования применяется для определения уровня детализации и времени, затрачиваемого на тестирование конкретного компонента. Например, на тестирование классов, более важных для приложения, отводится больше времени.

Рис. 1.6. В каком объеме тестировать
Мы будем использовать все перечисленные меры.
Ответы на поставленные вопросы и, возможно, на многие другие, оформляются в виде набора документов, принятого в компании. Например, тестовый план может содержать следующую информацию:
- Перечень тестовых ресурсов.
- Перечень функций и подсистем, подлежащих тестированию.
- Тестовую стратегию:
- Анализ функций и подсистем с целью определения слабых мест, требующих исчерпывающего тестирования, то есть участков функциональности, где появление дефектов наиболее вероятно.
- Определение стратегии выбора входных данных для тестирования. Поскольку в реальных применениях множество входных данных программного продукта практически бесконечно, выбор конечного подмножества для проведения тестирования является сложной задачей. Для ее решения могут быть применены методы покрытия классов входных и выходных данных, анализ крайних значений, покрытие случаев использования и тому подобное. Выбранная стратегия должна быть обоснована и задокументирована.
- Определение потребности автоматизации процесса тестирования. При этом решение об использовании существующей, либо о создании новой автоматизированной системы тестирования должно быть обосновано, а также продемонстрирована оценка затрат на создание новой системы или на внедрение уже существующей.
- График (расписание) тестовых циклов.
- Указание конкретных параметров аппаратуры и программного окружения.
- Определение тестовых метрик, которые необходимо собирать и анализировать, таких как покрытие набора требований, покрытие кода, количество и уровень серьезности дефектов, объем тестового кода и т.п.

Модульное тестирование на примере классов
Что тестировать?
Можно выделить два типа классов с точки зрения их взаимодействия с другими классами:
- примитивные классы;
- непримитивные классы.
| TBearingParam | Примитивный |
| TAxleParam | Примитивный |
| TCommand | Примитивный |
| TLog | Примитивный |
| TCommandQueue | Непримитивный |
| TStore | Непримитивный |
| TTerminalBearing | Непримитивный |
| TTerminalAxle | Непримитивный |
| TModel | Непримитивный |
| MainForm | Непримитивный |
Public. Члены с доступом public доступны из любых классов. Они образуют интерфейс класса, которым будет пользоваться любой разработчик, использующий данный класс в своем приложении.
Private. Члены с доступом private доступны только внутри самого класса, то есть из его методов. Они являются частью внутренней реализации класса и недоступны стороннему разработчику.
Protected. Члены с доступом protected доступны из самого класса и из классов, являющихся его потомками, но недоступны извне. Использование этих методов возможно только при создании класса- потомка, расширяющего функциональность базового класса.
Таким образом, необходимость тестирования функциональности класса зависит от того, предоставляется ли им возможность наследования. Если класс является законченным (final) и не предполагает наследования, необходимо тестирование его public части (впрочем, классы final не содержат protected членов). Если же класс рассчитан на расширение за счет наследования, необходимо тестирование также его protected части.
Кроме того, во многих языках класс может содержать статические (static) члены, которые принадлежат классу в целом, а не его конкретным экземплярам. При наличии public static или protected static членов, кроме тестирования объектов класса, должно отдельно выполняться тестирование статической части класса.
Как тестировать?
Как уже упоминалось, для тестирования классов применяются тестовые драйверы. Существует несколько способов реализации тестового драйвера:
Тестовый драйвер реализуется в виде отдельного класса. Методы этого класса создают объекты тестируемого класса и вызывают их методы, в том числе статические методы класса. Таким способом можно тестировать public часть класса.
Тестовый драйвер реализуется в виде класса, наследуемого от тестируемого. В отличие от предыдущего способа, такому тестовому драйверу доступна не только public, но и protected часть.
Тестовый драйвер реализуется непосредственно внутри тестируемого класса (в класс добавляются диагностические методы). Такой тестовый драйвер имеет доступ ко всей реализации класса, включая private члены. В этом случае в методы класса включаются вызовы отладочных функций и агенты, отслеживающие некоторые события при тестировании.
В дальнейшем мы будем использовать первый способ при реализации драйверов.
При разработке спецификации класса можно задействовать один из следующих подходов:
Контрактный подход. Интерфейс определяется в виде обязательств отправителя и получателя, вступивших во взаимодействие. Операция определяется как набор обязательств каждой стороны, причем ответственность по отношению друг к другу соблюдается как отправителем, так и получателем.
Подход защитного программирования. Интерфейс определяется главным образом в терминах получателя. Операция возвращает результат запроса - успешное или неудачное выполнение по конкретной причине (например, по недопустимому входному значению). Другими словами, соответствующий получатель следит за тем, чтобы на вход не попали некорректные данные, т.е. проверяет правильность и допустимость входных данных, и после получения запроса сообщает отправителю результат обработки запроса.
Различие между контрактным и защитным методами проектирования распространяется и на тестирование. Контрактное проектирование возлагает большую ответственность на проектировщика, чем на программы поиска ошибок. Основное внимание во время тестирования взаимодействий в условиях контактного подхода уделяется проверке того, выполнены ли объектом-отправителем предусловия методов получающего объекта. Не допускается построение тестовых случаев, нарушающих эти предусловия. Обычно практикуется перевод объекта- получателя в некоторое заданное состояние, после чего инициируется выполнение тестового драйвера, по условиям которого объект- отправитель требует, чтобы объект-получатель находился в другом состоянии. Смысл подобной проверки заключается в том, чтобы установить, выполняет ли объект-отправитель проверку предусловий объекта-получателя, прежде чем отправить заранее неприемлемое сообщение, и корректно ли он прекращает свою работу.
Как запустить тест?
Для того чтобы запустить тест, нужно:
- В методе Run тестового драйвера TCommandTester вызвать метод TCommandTest1, реализующий тест.
- Собрать и запустить приложение.
Кто, что, когда, как и в каком объеме?
Рассмотрим эти вопросы в контексте тестирования классов.
Кто выполняет тестирование? Обычно тестирование классов выполняют их разработчики. В этом случае время на изучение спецификации и реализации сводится к минимуму. Недостатком подхода является то, что если разработчик неправильно понял спецификации, то он для своей неправильной реализации разработает и "ошибочные" тестовые наборы.
Что тестировать? Необходимо удостовериться, что программный код класса в точности отвечает требованиям, сформулированным в его спецификации, и что он не делает ничего более.
В какой момент следует выполнять тестирование? План тестирования или хотя бы тестовые случаи должны разрабатываться после составления полной спецификации класса. Разработка тестовых случаев по мере реализации класса помогает разработчику лучше понять спецификацию. Тестирование класса должно проводиться до того, как возникнет необходимость использовать этот класс в других компонентах ПО. Регрессионное тестирование класса должно выполняться всякий раз, когда меняется реализация класса. Регрессионное тестирование позволяет убедиться в том, что разработанные и оттестированные функции продолжают удовлетворять спецификации после выполнения модификации ПО.
Как будет выполняться тестирование? Тестирование классов обычно выполняется путем разработки тестового драйвера, который создает экземпляры классов и окружает эти экземпляры соответствующей средой (тестовым окружением), чтобы стал возможен прогон соответствующего тестового случая. Драйвер посылает сообщения экземпляру класса в соответствии со спецификацией тестового случая, а затем проверяет исход этих сообщений. Тестовый драйвер должен удалять созданные им экземпляры тестируемого класса. Статические элементы данных класса также необходимо тестировать.
Какие объемы тестирования следует считать адекватными? Адекватность может быть измерена полнотой охвата тестами спецификации или реализации. Будем использовать оба способа.
Модульное тестирование на примере классов
Цель тестирования программных модулей состоит в том, чтобы удостовериться, что каждый модуль соответствует своей спецификации. Если это так, то причиной любых ошибок, которые возникают при их объединении, является неправильная стыковка модулей. В процедурно- ориентированном программировании модулем называется процедура или функция, иногда группа процедур, которая реализует абстрактный тип данных. Тестирование модулей обычно представляет собой некоторое сочетание проверок и прогонов тестовых случаев. Можно составить план тестирования модуля, в котором учесть тестовые случаи и построение тестового драйвера.
Тестирование классов аналогично тестированию модулей. Основным элементом объектно-ориентированной программы является класс. Рассмотрим методику тестирования отдельного класса. Тестирование классов охватывает виды деятельности, ассоциированные с проверкой реализации класса на точное соответствие спецификации класса. Если реализация корректна, то каждый экземпляр этого класса ведет себя подобающим образом.
Эффективного тестирования классов можно достичь при помощи ревью и тестовых прогонов. Ревью представляет собой просмотр исходного кода ПО с целью обнаружения ошибок и дефектов, возможно, до того, как это ПО заработает. Ревьюирование предназначено для выявления таких ошибок, как неспособность выполнять то или иное требование спецификации или ее неправильное понимание, а также алгоритмических ошибок в реализации. Тестовый прогон обеспечивает тестирование ПО в процессе выполнения программы. Осуществляя прогон программы, тестировщик стремится определить, способна ли программа вести себя в соответствии со спецификацией. Тестировщик должен выбрать наборы входных данных, определить соответствующие им правильные наборы выходных данных и сопоставить их с реально получаемыми выходными данными.
Рассмотрим тестирование классов в режиме прогона тестовых случаев. После идентификации тестовых случаев для класса нужно реализовать тестовый драйвер, обеспечивающий прогон каждого тестового случая, и запротоколировать результаты каждого прогона. При тестировании классов тестовый драйвер создает один или большее число экземпляров тестируемого класса и осуществляет прогон тестовых случаев. Тестовый драйвер может быть реализован как автономный тестирующий класс.
Подробное описание тестового случая
Рассматривается пример тестов на C# для класса TCommand (приложение 3 (HLD)). При выполнении заданий необходимо будет самостоятельно написать тесты для других классов приложения. Параллельно с изучением этого раздела полезно открыть проект ModuleTesting\ModuleTests.sln.
Рассмотрим тестирование класса TCommand. Этот класс реализует единственную операцию GetFullName(), которая возвращает полное название команды в виде строки. Разработаем спецификацию тестового случая для тестирования метода GetFullName на основе спецификации этого класса (приложение 3):
| Название класса: TСommand | Название тестового случая: TСommandTest1 |
| Описание тестового случая: Тест проверяет правильность работы метода GetFullName - получения полного названия команды на основе кода команды. В тесте подаются следующие значения кодов команд (входные значения): -1, 1, 2, 4, 6, 20, где -1 - запрещенное значение | |
| Начальные условия: Нет |
| Ожидаемый результат: Перечисленным входным значениям должны соответствовать следующие выходные: Коду команды -1 должно соответствовать сообщение "ОШИБКА: Неверный код команды" Коду команды 1 должно соответствовать полное название команды "ПОЛУЧИТЬ ИЗ ВХОДНОЙ ЯЧЕЙКИ" Коду команды 2 должно соответствовать полное название команды "ОТПРАВИТЬ ИЗ ЯЧЕЙКИ В ВЫХОДНУЮ ЯЧЕЙКУ" Коду команды 4 должно соответствовать полное название команды "ПОЛОЖИТЬ В РЕЗЕРВ" Коду команды 6 должно соответствовать полное название команды "ПРОИЗВЕСТИ ЗАНУЛЕНИЕ" Коду команды 20 должно соответствовать полное название команды "ЗАВЕРШЕНИЕ КОМАНД ВЫДАЧИ" |
public class Log { static private StreamWriter log=new StreamWriter("log.log"); //Создание лог файла static public void Add(string msg) //Добавление сообщения в лог файл { log.WriteLine(msg); } static public void Close() //Закрыть лог файл { log.Close(); } } abstract class Tester { protected void LogMessage(string s) //Добавление сообщения в лог-файл { Log.Add(s); } } class TCommandTester:Tester // Тестовый драйвер { TCommand OUT; public TCommandTester() { OUT=new TCommand(); Run(); } private void Run() { TCommandTest1(); } private void TCommandTest1() { int[] commands = {-1, 1, 2, 4, 6, 20}; for(int i=0;i<=5;i++) { OUT.NameCommand=commands[i]; LogMessage(commands[i].ToString()+" : "+OUT.GetFullName()); } } [STAThread] static void Main() { TCommandTester CommandTester = new TCommandTester(); Log.Close(); } } Листинг 2.1.
Тестовый драйверКласс TCommandTester содержит метод TCommandTest1(), в котором реализована вся функциональность теста. В данном случае для покрытия спецификации достаточно перебрать следующие значения кодов команд: -1, 1, 2, 4, 6, 20, где -1 - запрещенное значение, и получить соответствующие им полное название команды с помощью метода GetFullName(). Пары соответствующих значений заносятся в log-файл для последующей проверки на соответствие спецификации.
Таким образом, для тестирования любого метода класса необходимо:
- Определить, какая часть функциональности метода должна быть протестирована, то есть при каких условиях он должен вызываться. Под условиями здесь понимаются параметры вызова методов, значения полей и свойств объектов, наличие и содержимое используемых файлов и т. д.
- Создать тестовое окружение, обеспечивающее требуемые условия.
- Запустить тестовое окружение на выполнение.
- Обеспечить сохранение результатов в файл для их последующей проверки.
- После завершения выполнения сравнить полученные результаты со спецификацией.
Проверка результатов выполнения тестов (сравнение с ожидаемым результатом)
После завершения теста следует просмотреть текстовый журнал теста (..\ModuleTesting\bin\Debug\log.log), чтобы сравнить полученные результаты с ожидаемыми результатами, заданными в спецификации тестового случая TСommandTest1. Журнал теста:
-1 : ОШИБКА : Неверный код команды 1 : ПОЛУЧИТЬ ИЗ ВХОДНОЙ ЯЧЕЙКИ 2 : ОТПРАВИТЬ ИЗ ЯЧЕЙКИ В ВЫХОДНУЮ ЯЧЕЙКУ 4 : ПОЛОЖИТЬ В РЕЗЕРВ 6 : ПРОИЗВЕСТИ ЗАНУЛЕНИЕ 20 : ЗАВЕРШЕНИЕ КОМАНД ВЫДАЧИ
с приведенным примером необходимо самостоятельно
Для остальных примитивных классов (табл. 2.1) в соответствии с приведенным примером необходимо самостоятельно разработать спецификации тестовых случаев, соответствующие тесты и провести тестирование. Отчет требуется составить в следующей форме (табл. 2.2):
| Название тестового случая: |
| Тестировщик: |
| Тест пройден: Да/Нет (PASS/FAIL) |
| Степень важности ошибки: Фатальная (3 уровень - crash) Серьезная (2 уровень - расхождение в спецификации) Незначительная (1 уровень - незначительная ошибка) |
| Описание проблемы: |
| Как воспроизвести ошибку: |
| Предлагаемое исправление (необязательно): |
| Комментарий тестировщика (необязательно): |

Идентификация взаимодействий
Идентификация взаимодействий
Взаимодействие объектов представляет собой просто запрос одного объекта (отправителя) на выполнение другим объектом (получателем) одной из операций получателя и всех видов обработки, необходимых для завершения этого запроса.
В ситуациях, когда в качестве основы тестирования взаимодействий объектов выбраны только спецификации общедоступных операций, тестирование намного проще, чем когда такой основой служит реализация. Мы ограничимся тестированием общедоступного интерфейса. Такой подход вполне оправдан, поскольку мы полагаем, что классы уже успешно прошли модульное тестирование. Тем не менее, выбор такого подхода отнюдь не означает, что не нужно возвращаться к спецификациям классов, дабы убедиться в том, что тот или иной метод выполнил все необходимые вычисления. Это обусловливает необходимость проверки значений атрибутов внутреннего состояния получателя, в том числе любых агрегированных атрибутов, т.е. атрибутов, которые сами являются объектами. Основное внимание уделяется отбору тестов на основе спецификации каждой операции из общедоступного интерфейса класса.
Взаимодействия неявно предполагаются в спецификации класса, в которой установлены ссылки на другие объекты. В разделе 4 рассматривалось тестирование примитивных классов. Такие объекты представляют собой простейшие компоненты системы и, несомненно, играют важную роль при выполнении любой программы. Тем не менее, в объектно-ориентированной программе существует сравнительно небольшое количество примитивных классов, которые реалистично моделируют объекты задачи и все отношения между этими объектами. Обычным явлением для хорошо спроектированных объектно- ориентированных программ является использование непримитивных классов; в этих программах им отводится главенствующая роль.
Выявить такие взаимодействующие классы можно, используя отношения ассоциации (в том числе отношения агрегирования и композиции), представленные на диаграмме классов. Ассоциации такого рода преобразуются в интерфейсы класса, а тот или иной класс взаимодействует с другими классами посредством одного или нескольких способов:
Тип 1. Общедоступная операция имеет один или большее число формальных параметров объектного типа. Сообщение устанавливает ассоциацию между получателем и параметром, которая позволяет получателю взаимодействовать с этим параметрическим объектом.
Тип 2. Общедоступная операция возвращает значения объектного типа. На класс может быть возложена задача создания возвращаемого объекта, либо он может возвращать модифицированный параметр.
Тип 3. Метод одного класса создает экземпляр другого класса как часть своей реализации.
Тип 4. Метод одного класса ссылается на глобальный экземпляр некоторого другого класса. Разумеется, принципы хорошего тона в проектировании рекомендуют минимальное использование глобальных объектов. Если реализация какого-либо класса ссылается на некоторый глобальный объект, рассматривайте его как неявный параметр в методах, которые на него ссылаются.
Приведем еще раз таблицу разделения классов на примитивные и непримитивные типы (табл. 3.1):
| TBearingParam | Примитивный |
| TAxleParam | Примитивный |
| TCommand | Примитивный |
| TLog | Примитивный |
| TCommandQueue | Непримитивный |
| Класс | Непримитивный |
| TStore | Непримитивный |
| TTerminalBearing | Непримитивный |
| TTerminalAxle | Непримитивный |
| TModel | Непримитивный |
| MainForm | Непримитивный |
| TCommandQueue | 1 | 1 | 3 | 1 | 1 | ||||
| TStore | 3 | 1 | 1 | ||||||
| TTerminalBearing | 3 | ||||||||
| TTerminalAxle | 3 | ||||||||
| TModel | 3 | 3 | 3 | 3 | 3 | ||||
| MainForm | 3 |
Как запустить тест?
Для выполнения этого теста в методе Run необходимо вызвать метод TCommandQuеueTest1() и запустить программу на выполнение:
private void Run() { TCommandQueueTest1(); } Пример 3.3. Метод Run
Подробное описание тестового случая
Продемонстрируем тестирование взаимодействий на примере класса TCommandQueue. Из табл. 3.2, которая была составлена на основе спецификаций классов, описанных в приложении 2, видно, что класс очереди команд взаимодействует со следующими классами:
TBearingParam, TAxleParam, TCommand, TStore, TterminalBearing. С объектом TCommand осуществляется взаимодействие третьего типа, т. е. TCommandQueue создает объекты класса TCommand как часть своей внутренней реализации. С остальными классами осуществляется взаимодействие первого типа: ссылки на объекты классов TStore и TTerminalBearing передаются как параметры в конструктор TCommandQueue, а ссылки на объекты классов TBearingParam и TAxleParam передаются в метод TCommandQueue.AddCommand.
Одновременно с изучением этого раздела можно открыть проект IntegrationTesting\IntegrationTests.sln.
Для тестирования взаимодействия класса TCommandQueue и класса TСommand, так же, как и при модульном теcтировании, разработаем спецификацию тестового случая:
| Названия взаимодействующих классов: TСommandQueue, TCommand | Название теста: TCommandQueueTest1 |
| Описание теста: тест проверяет возможность создания объекта типа TCommand и добавления его в очередь при вызове метода AddCommand Начальные условия: очередь команд пуста Ожидаемый результат: в очередь будет добавлена одна команда | |
- Метод Init, в котором создаются объекты классов TStore, TterminalBearing и объект типа TcommandQueue. Этот метод необходимо вызывать в начале каждого теста, чтобы тестируемые объекты создавались вновь: private void Init() { TB = new TTerminalBearing(); S = new TStore(); CommandQueue=new TCommandQueue(S,TB); S.CommandQueue=CommandQueue; } Пример 3.1. Метод Init
- Методы, реализующие тесты. Каждый тест реализован в отдельном методе.
- Метод Run, в котором вызываются методы тестов.
- Метод dump, который сохраняет в log-файле теста информацию обо всех командах, находящихся в очереди в формате - номер позиции в очереди: полное название команды.
- Точку входа в программу - метод Main, в котором происходит создание экземпляра класса TCommandQueueTester и запуск метода Run.
private void TCommandQueueTest1() { Init(); LogMessage("///////// TCommandQueue Test1 /////////////"); LogMessage("Проверяем, создается ли объект типа TCommand"); // В очереди нет команд dump(); // Добавляем команду // параметр = -1 означает, что команда должна быть добавлена //в конец очереди CommandQueue.AddCommand(TCommand.GetR,0,0,0,new TBearingParam(),new TAxleParam(),-1); LogMessage("Command added"); // В очереди одна команда dump(); } Пример 3.2. Тест, проверяющий создание объекта типа TСommandВ этот класс включены еще два разработанных теста.
Проверка результатов выполнения тестов (сравнение с ожидаемым результатом)
После завершения теста следует просмотреть текстовый журнал теста (..\IntegrationTesting\bin\Debug\test.log), чтобы сравнить полученные результаты с ожидаемыми результатами, заданными в спецификации тестового случая TCommandQueueTest1.
//////////////////// TCommandQueue Test1 ////////////////// Проверяем, создается ли объект типа TCommand 0 commands in command queue Command added 1 commands in command queue 0 : ПОЛУЧИТЬ ИЗ ВХОДНОЙ ЯЧЕЙКИ Пример 3.4. Журнал теста
Выбор тестовых случаев
Исчерпывающее тестирование, другими словами, прогон каждого возможного тестового случая, покрывающего каждое сочетание значений - это, вне всяких сомнений, надежный подход к тестированию. Однако во многих ситуациях количество тестовых случаев достигает таких больших значений, что обычными методами с ними справиться попросту невозможно. Если имеется принципиальная возможность построения такого большого количества тестовых случаев, на построение и выполнение которых не хватит никакого времени, должен быть разработан систематический метод определения, какими из тестовых случаев следует воспользоваться. Если есть выбор, то мы отдаем предпочтение таким тестовым случаям, которые позволяют найти ошибки, в обнаружении которых мы заинтересованы больше всего.
Существуют различные способы определения, какое подмножество из множества всех возможных тестовых случаев следует выбирать. При любом подходе мы заинтересованы в том, чтобы систематически повышать уровень покрытия.
Для тестирования взаимодействия остальных непримитивных
Для тестирования взаимодействия остальных непримитивных классов (табл. 2.1) по аналогии с приведенным примером требуется самостоятельно разработать спецификации тестовых случаев, соответствующие тесты, провести тестирование и составить тестовые отчеты (табл. 2.2).

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

Описание случая использования (use case) "подбор подшипников для оси"
Последовательно приходят два подшипника, поступает запрос от оси. При поступлении запроса от оси система подбирает два подшипника из имеющихся на складе и выдает их в выходную ячейку.
Рассмотрим этот случай использования подробнее. Согласно спецификации, система постоянно опрашивает склад и терминал оси. При поступлении подшипника (статус склада 32) система опрашивает терминал подшипника, формирует и посылает команду складу "принять подшипник" и получает ответ от склада о результатах выполнения команды. При поступлении оси (поступлении параметров оси при опросе терминала оси) система должна подобрать подшипники из имеющихся на складе, сформировать команды для их выдачи, послать их складу и получить ответ о результате выполнения команд.
Далее приводится пошаговое описание этого случая использования:
Приняли на склад первый подшипник (1-10)
Приняли на склад второй подшипник (11-20)
Поступила ось (21-26)
Подбираем первый подшипник для оси (27-30)
Подбираем второй подшипник для оси (31-34)
Завершение выдачи команд (35-39).
Пошаговое описание случая использования
Пошаговое описание приведено на рис. 4.2.

Рис. 4.2. Пример use case
Системное тестирование
Системное тестирование качественно отличается от интеграционного и модульного уровней. Системное тестирование рассматривает систему в целом и применяется на уровне пользовательских интерфейсов, в отличие от последних фаз интеграционного тестирования, которое оперирует на уровне интерфейсов модулей, хотя набор модулей может быть аналогичным. Различны и цели этих уровней тестирования. На уровне системы часто сложно и малоэффективно анализировать прохождение тестовых траекторий внутри программы, а также отслеживать правильность работы конкретных функций. Основной задачей системного тестирования является выявление дефектов, связанных с работой системы в целом, таких как неверное использование ресурсов системы, непредусмотренные комбинации данных пользовательского уровня, несовместимость с окружением, непредусмотренные сценарии использования, отсутствующая или неверная функциональность, неудобство в использовании и тому подобное.
Поскольку системное тестирование проводится на уровне пользовательских интерфейсов, то построение специальной тестовой системы становится технически необязательным. Однако объемы данных на этом уровне таковы, что обычно более эффективным подходом является полная или частичная автоматизация тестирования, что может потребовать создания тестовой системы намного более сложной, чем система тестирования на уровне модулей или их комбинаций.
Необходимо подчеркнуть, что существует два принципиально разных подхода к системному тестированию.
В первом варианте для построения тестов используются требования к системе, например, для каждого требования строится тест, который проверяет выполнение данного требования в системе. Этот подход особенно широко применяется при разработке военных и научных систем, когда заказчик вполне осознает, какая функциональность ему нужна, и составляет полный набор формальных требований. Тестировщик в данном случае только проверяет, соответствует ли разработанная система этому набору. Такой подход предполагает длинную и дорогостоящую фазу сбора требований, выполняемую до начала собственно проекта.
В этом случае для определения требований обычно разрабатывается прототип будущей системы.
Во втором подходе основой для построения тестов служит представление о способах использования продукта и о задачах, которые он решает. На основе более или менее формальной модели пользователя создаются случаи использования системы, по которым затем строятся собственно тестовые случаи. Случай использования (use case) описывает, как субъект использует систему, чтобы выполнить ту или иную задачу. Субъекты или актеры (actors) могут исполнять различные роли при работе с системой. Случаи использования могут описываться с различной степенью абстракции. Случаи использования не обязательно охватывают каждое требование. Можно конкретизировать случаи использования и расширять их в наборы более специфических случаев использования (пошаговое описание случая использования). В контексте конкретного случая использования можно определить один или большее число сценариев. Сценарий представляет конкретный экземпляр случая использования - путь в пошаговом описании случая использования. Каждый путь (сценарий) в случае использования должен быть протестирован (рис. 4.1).

Рис. 4.1. Тестирование случаев использования
Входные данные для каждого сценария надо выбирать следующим образом:
- Идентифицировать все значения (входные данные), которые могут задавать субъекты для случая использования.
- Определить классы эквивалентности для каждого типа входных данных.
- Построить таблицу со списком значений из различных классов эквивалентности.
- Построить тестовые случаи на базе таблицы с учетом внешних ограничений.
- На основе требований определить случаи использования (use case)
- На основе каждого случая использования (use case) построить сценарии.
- Для каждого сценария разработать тестовые случаи (набор тестов).
в очередь команд склада на
Состояние окружения (входные данные):
Статус склада (StoreStat=32). Пришел подшипник.
Статус обмена с терминалом подшипника (0 - есть подшипник) и его параметры (RollerPar="0 NewUser Depot1 123456 1 12 1 1").
Статус обмена с терминалом оси (1 - нет оси) и ее параметры (AxlePar="1 NewUser Depot1 123456 1 0 12 12").
Статус команды (CommandStatus=0). Команда успешно принята.
Сообщение от склада (StoreMessage=1). Команда успешно выполнена.
Ожидаемая последовательность событий (выходные данные):
Система запрашивает статус склада (вызов функции GetStoreStat) и получает 32.
Система запрашивает параметры подшипника (вызов функции GetRollerPar) и получает 0 NewUser Depot1 123456 1 12 1 1.
Система запрашивает параметры оси (вызов функции GetAxlePar) и получает 1 NewUser Depot1 123456 1 0 12 12.
Система добавляет в очередь команд склада на последнее место команду SendR (получить из приемника в ячейку) (вызов функции SendStoreCom) и получает сообщение о том, что команда успешно принята - 0.
Система запрашивает склад о результатах выполнения команды (вызов функции GetStoreMessage) и получает сообщение о том, что команда успешно выполнена - 1.
Система запрашивает статус склада (вызов функции GetStoreStat) и получает 32.
Система запрашивает параметры подшипника (вызов функции GetRollerPar) и получает 0 NewUser Depot1 123456 1 12 1 1.
Система запрашивает параметры оси (вызов функции GetAxlePar) и получает 1 NewUser Depot1 123456 1 0 12 12.
Система добавляет в очередь команд склада на первое место команду GetR (получить из приемника в ячейку) (вызов функции SendStoreCom) и получает сообщение о том, что команда успешно принята - 0.
Система запрашивает склад о результатах выполнения команды (вызов функции GetStoreMessage) и получает сообщение о том, что команда успешно выполнена - 1.
Изменяем состояние окружения (входные данные):
Статус обмена с терминалом подшипника (1 - нет подшипника) и его параметры (RollerPar="1 NewUser Depot1 123456 1 12 1 1").
Статус обмена с терминалом оси (0 - есть ось) и ее параметры (AxlePar="0 NewUser Depot1 123456 1 0 12 12").
Ожидаемая последовательность событий (выходные данные):
Система запрашивает статус склада (вызов функции GetStoreStat) и получает 32.
Система запрашивает параметры подшипника (вызов функции GetRollerPar) и получает 1 NewUser Depot1 123456 1 12 1 1.
Система запрашивает параметры оси (вызов функции GetAxlePar) и получает 0 NewUser Depot1 123456 1 0 12 12.
Система добавляет в очередь команд склада на последнее место команду SendR (вызов функции SendStoreCom) и получает сообщение о том, что команда успешно принята - 0.
Система запрашивает склад о результатах выполнения команды (вызов функции GetStoreMessage) и получает сообщение о том, что команда успешно выполнена - 1.
Система добавляет в очередь команд склада на последнее место команду SendR (вызов функции SendStoreCom) и получает сообщение о том, что команда успешно принята - 0.
Система запрашивает склад о результатах выполнения команды (вызов функции GetStoreMessage) и получает сообщение о том, что команда успешно выполнена - 1.
Система добавляет в очередь команд склада на последнее место команду Term (вызов функции SendStoreCom) и получает сообщение о том, что команда успешно принята - 0.
Система запрашивает склад о результатах выполнения команды (вызов функции GetStoreMessage) и получает сообщение о том, что команда успешно выполнена - 1.
Во всех последующих разделах будет подробно рассматриваться именно этот тестовый случай!
Список альтернативных путей
В пошаговом описании случая использования рассматривался оптимистический сценарий, например при анализе статуса склада в пунктах 3 и 11 считалось, что поступил подшипник. Но необходимо рассмотреть и возможные альтернативные сценарии:
3, 11 - анализ статуса склада:
3.a, 11.a - подшипник в манипуляторе.
3.b, 11.b - склад свободен.
3.c, 11.c - ошибочное состояние.
При получении сообщения от склада о выполнении команды считалось, что команда выполнена без ошибки, но здесь тоже существуют альтернативные варианты:
8, 16, 22, 24, 26 - получение сообщения о выполнении команды:
8.a, 16.a, 22.a, 24.a, 26.a - нет склада.
8.b, 16.b, 22.b, 24.b, 26.b - нет сообщения.
8.с, 16.c, 22.c, 24.c, 26.c - команда выполнена с ошибкой.
Список альтернативных вариантов для рассмотренного случая использования можно продолжить.
Ручное тестирование
Как запустить тест
Для того чтобы запустить тест, нужно:
В методе Class1.Main создать экземпляр нового класса и вызвать его метод start. Так как метод start является виртуальным, то можно обращаться к тестам, используя ссылку на тип Test. Это делается так:
Test t = new <ваш класс-потомок Test>; t.start(); Собрать и запустить приложение.
Описание тестовых процедур
Тестовые процедуры - это формальный документ, содержащий описание необходимых шагов для выполнения тестового набора. В случае ручных тестов тестовые процедуры содержат полное описание всех шагов и проверок, позволяющих протестировать продукт и вынести вердикт PASS/FAIL. Процедуры должны быть составлены таким образом, чтобы любой инженер, не связанный с данным проектом, был способен адекватно провести цикл тестирования, обладая только самыми базовыми знаниями о применяющемся инструментарии.
с настоящим пунктом полезно предварить
Ознакомление с настоящим пунктом полезно предварить изучением п. 7, содержащего описание ручного тестирования. Здесь рассматривается та часть теста на C#, которую вам придется написать самостоятельно при выполнении заданий. Приведенный пример был разработан в соответствии со спецификацией тестового случая N1. Для простоты будем считать, что события происходят последовательно в строго заданном порядке. Реально наша система представляет собой многопоточное приложение, поэтому мы не можем это гарантировать.
class Test1:Test { override public void start() {//Задаем состояние окружения (входные данные) StoreStat="32"; //Поступил подшипник RollerPar="0 NewUser Depot1 123456 1 12 1 1"; //статус обмена с терминалом подшипника (0 - есть подшипник) //и его параметры AxlePar="1 NewUser Depot1 123456 1 0 12 12"; //статус обмена с терминалом оси (1 - нет оси) и ее параметры CommandStatus="0"; //команда успешно принята StoreMessage="1"; //успешно выполнена //Получаем информацию о функционировании системы wait("GetStoreStat"); //опрос статуса склада wait("GetRollerPar"); //Получение информации о подшипнике с терминала подшипника wait("GetAxlePar"); //Получение информации об оси с терминала оси wait("SendStoreCom"); //добавление в очередь команд склада на первое место //команды GetR (получить из приемника в ячейку) wait("GetStoreMessage"); //Получение сообщения от склада о результатах выполнения команды //В результате первый подшипник должен быть принят wait("GetStoreStat"); //опрос статуса склада wait("GetRollerPar"); //Получение информации о подшипнике с терминала подшипника wait("GetAxlePar"); //Получение информации об оси с терминала оси wait("SendStoreCom"); //добавление в очередь команд склада на первое место //команды GetR (получить из приемника в ячейку) wait("GetStoreMessage"); //Получение сообщения от склада о результатах выполнения //команды.
В результате второй подшипник должен быть принят //Задаем новое состояние окружения (входные данные) RollerPar="1 NewUser Depot1 123456 1 12 1 1"; //статус обмена с терминалом подшипника (1 - нет подшипника) //и его параметры AxlePar="0 NewUser Depot1 123456 1 0 12 12"; //статус обмена с терминалом оси (0 - есть ось) и ее параметры //Получаем информацию о функционировании системы wait("GetStoreStat"); //опрос статуса склада wait("GetRollerPar"); //Получение информации о подшипнике с терминала подшипника wait("GetAxlePar"); //Получение информации об оси с терминала оси wait("SendStoreCom");//Добавление в очередь команд склада на последнее место //команды SendR (ячейку на выход) wait("GetStoreMessage"); //Получение сообщения от склада о результатах выполнения //команды //В результате первый подшипник для оси должен быть выдан wait("SendStoreCom"); //Добавление в очередь команд склада на последнее место //команды SendR (ячейку на выход) wait("GetStoreMessage"); //Получение сообщения от склада о результатах выполнения //команды. //В результате второй подшипник для оси должен быть выдан wait("SendStoreCom"); //Добавление в очередь команд склада на последнее место //команды Term (завершение команд выдачи) wait("GetStoreMessage"); //Получение сообщения от склада о результатах выполнения //команды finish(); } } Пример 5.1. Пример фрагмента теста (вариант 1)При разработке тестов не обязательно дожидаться каждого события, которое должно происходить в соответствии со случаем использования. Достаточно вызвать wait для событий, после наступления которых надо менять состояние окружения. В период ожидания наступления события, заданного в wait, может происходить любое количество других событий. Все эти события будут занесены в журнал. При необходимости ждать не первого, а n-го вызова можно вызывать wait с одним и тем же параметром n раз (например, в цикле). При таком подходе тест будет гораздо короче, например приведенный выше тест будет выглядеть следующим образом:
class Test1:Test { override public void start() { StoreStat="32";//Пришел подшипник RollerPar="0 NewUser Depot1 123456 1 12 1 1";//его параметры AxlePar="1 NewUser Depot1 123456 1 0 12 12";//нет оси CommandStatus="0";//команда успешно принята StoreMessage="1";//команда успешно выполнена wait("SendStoreCom");//первый подшипник принят wait("SendStoreCom");//второй подшипник принят RollerPar="1 NewUser Depot1 123456 1 12 1 1"; //больше нет подшипников AxlePar="0 NewUser Depot1 123456 1 0 12 12";//есть ось wait("SendStoreCom");//выдача подшипника wait("SendStoreCom");//выдача подшипника wait("SendStoreCom");//завершение выдачи finish();} } Пример 5.2. Пример фрагмента теста (вариант 2)
Пример неправильного теста
Предположим, что вы не поняли Приложение 2 (FS) во всех деталях и неправильно составили спецификацию тестового случая №1 и тест. Например:
//Задаем состояние окружения (входные данные) StoreStat="32"; //Поступил подшипник ... //Получаем информацию о функционировании системы wait("GetStoreStat"); //опрос статуса склада //Вместо того, чтобы получить информацию о подшипнике с //терминала подшипника, мы хотим получить информацию //об оси с терминала оси wait("GetAxlePar"); ... В журнале теста мы увидим следующую информацию:
CALL: GetStoreStat 0 RETURN: 32 CALL: GetRollerPar RETURN: 0 NewUser Depot1 123456 1 12 1 1 CALL: GetAxlePar RETURN: 1 NewUser Depot1 123456 1 0 12 12 ... Главное - суметь разобраться и исправить спецификацию тестового случая и тест, если вы сами допустили ошибку.
Проверка результатов выполнения тестов (сравнение с ожидаемым результатом)
После завершения теста следует просмотреть текстовый журнал теста (..\SystemTesting\ManualTests\Tests\bin\Debug\log.log), чтобы выяснить, какая последовательность событий в системе была реально зафиксирована (выходные данные) и сравнить их с ожидаемыми результатами, заданными в спецификации тестового случая №1.
Как уже упоминалось, кроме журнала теста, создается еще и соответствующий журнал системы. В обоих журналах содержится информация о происходивших в системе событиях. До того, как вручную сравнивать журнал теста с ожидаемыми результатами, заданными в спецификации тестового случая, вы можете использовать SystemLogAnimator (п. 14) для визуализации соответствующего журнала системы и получить наглядное (в том числе ретроспективное) представление о том, как функционировала система. Это важно при разработке тестовых случаев, потому что, не зная в деталях, как работает система, можно написать неправильный тест. Необходимо научиться отличать, что вы в действительности обнаружили - ошибку в системе или результат работы неправильного теста.
Ручное тестирование
Наиболее распространенным способом разработки тестов является создание тестового кода вручную. Такой способ создания тестов является наиболее гибким, однако производительность тестировщиков при создании тестового кода соизмерима с производительностью разработчиков при создании кода продукта, а объемы тестового кода часто бывают в 1-10 раз больше объема самого продукта.
В этом случае запуск тестов осуществляется вручную. Проверку, прошла ли тестируемая система испытания на заданном тестовом случае, тестировщик также осуществляет вручную, сравнивая фактические результаты журнала теста c ожидаемыми результатами, описанными в спецификации тестового случая.
Функции dll-библиотеки обеспечивают обращение к серверу для получения информации о состоянии элементов комплекса и возвращают серверу информацию о функционировании системы. Значит, для моделирования состояния окружения (входных данных) необходимо создать специальный сервер.
Кроме того, необходимо сохранять получаемую от сервера информацию о функционировании системы (выходные данные) в журнале (рис. 5.1).

Рис. 5.1. Система и ее окружение (ручное тестирование)
При разработке тестов был использован следующий подход:
- состояние окружения задается в тесте (входные данные);
- в тесте создается сервер:
- сервер по запросу от dll передает информацию о заданном состоянии окружения;
- сервер получает от dll информацию о функционировании системы;
- получаемая информация сохраняется в журнале теста.
необходимо составить полный список
Для тестового случая № 1 необходимо составить полный список всех возможных альтернативных путей (см. подраздел "Список альтернативных путей") и разработать соответствующие тесты.
Кроме того, необходимо:
- выбрать случай использования на основании дерева решений (..\SystemTesting\Decision Tree.vsd);
- составить пошаговое описание выбранного случая использования;
- учесть все альтернативные пути;
- составить спецификации тестовых случаев;
- разработать соответствующие тестовые случаи (тесты);
- составить тестовые отчеты (табл. 2.2).

Автоматизация тестирования с помощью скриптов
Автоматизация тестирования с помощью скриптов
Общая тенденция последнего времени предусматривает максимальную автоматизацию тестирования, которая позволяет справляться с большими объемами данных и тестов, необходимых для современных продуктов.
В этом случае запуск тестов и проверка того, что тестируемая система прошла испытания на заданном тестовом случае, будет осуществляться автоматически.
Здесь еще раз повторим, что функции в dll были переписаны так, что они обращаются к серверу для получения информации о состоянии элементов комплекса и возвращают серверу информацию о функционировании системы. Для задания состояния окружения (входных данных) необходимо обратиться к серверу и передавать ему необходимую информацию. В данном случае был разработан сервер, который кроме приема и передачи информации еще осуществляет проверку правильности поведения системы (рис. 6.1). Он представляет собой модель тестируемой системы. В рамках модели заданы ожидаемые результаты и осуществляется сравнение выходных данных с ожидаемыми результатами. Хотя считалось, что разработанная модель является корректной, полной и непротиворечивой, у вас есть реальная возможность найти ошибки и в самой модели.
При разработке тестов был использован следующий подход:
- состояние окружения задается в тесте (входные данные);
- разработанный сервер:
- передает информацию о заданном состоянии окружении по запросу от dll;
- получает от dll информацию о функционировании системы (выходные данные);
- сравнивает выходные данные с ожидаемым результатом.
- получаемая информация сохраняется в журнале теста;
- строится таблица покрытия FS (..\SystemTesting\ScriptTests\Logs\summary.html).
Как запустить тест
Запустить run.bat. В файле run.bat запускается tests.bat. Файл tests.bat содержит команды запуска тестов и установки необходимого состояния базы данных:
osql -H