3. Тестирование программы


3.1 Основные термины и их соотношение
3.2 Статические методы устранения ошибок
3.3 Функциональное тестирование
3.4 Структурное тестирование



3 Тестирование и отладка программного обеспечения

3.1 Основные термины и их соотношение

Отладка – процесс изменения программы с целью исправления ошибок. Цель отладки – получение работоспособной программы.
Тест – совокупность специально подобранных входных и соответствующих им выходных данных, используемая для контроля правильности программы, или, проще, заранее заготовленная пара «вход – выход». Выходные данные теста формируются вручную или программным путем, но только не с помощью тестируемой программы.
Содержание тестов определяется спецификацией задачи и логикой ее решения.
Тестирование – исполнение программы на наборе тестов на компьютере. Тестирование является основным процессом, обеспечивающим надежность программы. Его цель – проверка правильности функционирования программы и выявление возможных ошибок.
Принципиальная трудность проектирования тестов состоит в практической невозможности составления всех тестовых наборов данных для всех возможных режимов работы разрабатываемой программной системы. Поэтому задача проектирования тестов сводится к проектированию ограниченного их набора, гарантирующего с достаточной достоверностью правильную работу программы во всех практически значимых режимах.
Отладка включает тестирование, так как предполагает анализ и исправление программы без ее выполнения (анализ без применения компьютера и исправление синтаксических ошибок в результате компиляции). Так, в кн. Р. Гласса[1] все методы поиска ошибок подразделяются на статические, включающие анализ программы без ее выполнения, и методы тестирования.
Понятие отладки применимо к программе на стадии разработки.
Приведенные понятия теста и тестирования относятся к программе в целом, независимо от аспекта ее рассмотрения (как «черного ящика» или с точки зрения структуры; тестирование разработчиком или заказчиком).

Виды тестов
Функциональные тесты составляются на уровне спецификации, до решения задачи. Будущий алгоритм рассматривается как «черный ящик» - функция с неизвестной (или не рассматриваемой) структурой, преобразующая входы в выходы. Суть функциональных тестов: каким бы способом ни решалась задача, при заданных входных значениях должны получиться соответствующие выходные значения.
Структурные тесты составляются для проверки логики решения, или логики работы уже готового алгоритма. Логика определяется последовательностью операций, их условным выполнением или повторением (т.е. композицией базовых конструкций). Совокупность структурных тестов должна обеспечить проверку каждой из таких конструкций.
Чаще всего совокупность тщательно составленных функциональных тестов покрывает множество структурных тестов.
Приведенные понятия различаются тем, что первое рассматривает программу только с точки зрения входов и выходов, тогда как второе относится к ее структуре; но оба понятия не касаются процесса организации тестирования.



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


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

1.4        








1.5       3.2 Статические методы устранения ошибок


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

Проверка за столом
Этот метод наиболее традиционен, но при этом совершенно необходим, хотя такая проверка не вызывает восторга и ее иногда стараются избежать.
Термин «проверка за столом» характеризует неавтоматизированную деятельность, к которой наиболее часто относят:
-    анализ текста программы на наличие ошибок;
-    выполнение программы вручную («сухую прокрутку»).

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

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

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

1.6        

1.7        

1.8       3.3 Функциональное тестирование


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

Разбиение на классы эквивалентности
Это самый популярный способ. Его суть заключается в разделении области входных данных программы на классы эквивалентности и разработке для каждого класса одного тестового варианта.
Класс эквивалентности – набор данных с общими свойствами, в силу чего при обработке любого набора данных этого класса задействуется один и тот же набор операторов[2].
Классы эквивалентности определяются по спецификации программы. Тесты строятся в соответствии с классами эквивалентности, а именно: выбирается вариант исходных данных некоторого класса и определяются соответствующие выходные данные.
Самыми общими классами эквивалентности являются классы допустимых и недопустимых (аномальных) исходных данных. Описание класса строится как комбинация условий, описывающих каждое входное данное.
Условия допустимости или недопустимости данных задают возможные значения данных и могут описывать:
2.     некоторое конкретное значение; определяется один допустимый и два недопустимых класса эквивалентности: заданное значение, множество значений меньше заданного, множество значений больше заданного;
3.     диапазон значений; определяется один допустимый и два недопустимых класса эквивалентности: множество значений в границах диапазона; множество значений, выходящих за левую границу диапазона; множество значений, выходящих за правую границу диапазона;
4.     множество конкретных значений; определяется один допустимый и один недопустимый класс эквивалентности: заданное множество и множество значений, в него не входящих.
Такие классы можно описать языком логики, например, языком исчисления предикатов. Описания более сложных условий и соответствующих классов (например, элементы массива должны находиться в некотором диапазоне и при этом массив не должен содержать нулевых элементов) могут быть построены на основании приведенных выше условий.
В примере, приводимом в вопросе 2 темы 3, при построении тестов неформально использовался описанный метод. В методических целях были выделены только основные класс тестов. Кроме того, исходя из условия задачи, были выделены классы эквивалентности внутри класса правильных данных.

Анализ граничных значений
Этот способ построения тестов дополняет предыдущий и предполагает анализ значений, лежащих на границе допустимых и недопустимых данных. Построение таких тестов часто диктуется интуицией.
Основные правила построения тестов:
1.     если условие правильности данных задает диапазон, то строятся тесты для левой и правой границы диапазона; для значений чуть левее левой и чуть правее правой границы;
2.     если условие правильности данных задает дискретное множество значений, то строятся тесты для минимального и максимального значений; для значений чуть меньше минимума и чуть больше максимума;
3.     если используются структуры данных с переменными границами (массивы), то строятся тесты для минимального и максимального значения границ.
Согласно этим правилам для упомянутого примера следовало бы построить тесты для значений данных, лежащих на границах диапазонов; кроме того, следовало бы рассмотреть массив из одного и из 50 элементов и, соответственно, матрицу из такого же количества строк и столбцов.

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








3.4       Структурное тестирование


Структурное тестирование –  тестирование логики готовой программы или ее частей.  Логика программы определяется управляющими конструкциями, а качество структурного тестирования – степенью, в которой тесты обеспечивают проверку (т.е. прохождение) всевозможных путей программы.
Задача построения тестов весьма сложна. При структурном подходе, о котором и идет речь, возможно построение некоторого формального метода разработки тестов и соответствующего формального аппарата (базирующегося, например, на логике предикатов). Такие методы существуют, но они очень сложны и могут быть применены только к простым алгоритмам. Реальные методы включают формальное средства, эмпирические положения и рекомендации, и, как правило, при описании опираются на примеры[3].

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

Тестирование всех путей
Предполагает выполнение каждого пути в программе. Поскольку в программе с циклами выполнение каждого пути обычно нереализуемо, то этот критерий не может считаться перспективным.
Если отказаться от полного тестирования всех путей, то приведенный критерий сведется к требованию выполнения каждого оператора программы по крайней мере один раз. Это слабый критерий. Проиллюстрируем это. Пусть имеется следующий фрагмент алгоритма, где A, B, X – числовые переменные:
        если ((A>1) & (B=0)) то
           X:=X/A;
        кесли;
        если ((A=2) Ú (X>1)) то
           X:=X+1;
        кесли;
Чтобы обозначить все пути, запишем в квадратных скобках пустую ветвь «иначе». Пути обозначим слева малыми латинскими буквами. Пусть a – путь, предшествующий данному фрагменту.
     a          ........................
           если ((A>1) & (B=0)) то
     c          X:=X/A
           [иначе
     b
           ]
           кесли;
           если ((A=2) Ú (X>1)) то
     e          X:=X+1
           [иначе
     d
           ]
           кесли;
Рис. 4.1. Пример для иллюстрации критериев тестирования
На тесте A=2, B=0, X=6 каждый оператор выполняется. Согласно приведенному критерию, этого теста достаточно. Однако при этом тестирование по такому критерию не обнаруживает целого ряда ошибок. Так, если во втором условии записать X<1, то эта ошибка не будет обнаружена. Кроме того, здесь возможных путей четыре: abd, abe, acd, ace, тогда как тестируется только один из них.
Этот критерий ввиду его слабости обычно не используют.

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

        если ((A=2) Ú (X>1)) то
           X:=X+1
        кесли;
согласно рассматриваемому критерию достаточно теста A=2, X=2. Но, как и при рассмотрении предыдущего критерия,  если во втором условии записать X<1, то эта ошибка не будет обнаружена.


 Комбинаторное покрытие условий
Каждое простое условие (логическая переменная или отношение, входящие в сложное условие) должно выполниться по крайней мере один раз. Это наиболее сильный критерий, которым мы и будем пользоваться.
Например, в конструкции     
        если ((A=2) Ú (X>1)) то
           X:=X+1
        кесли;
для  прохождения ветви «то» может быть построено три теста, где  истинно только одно из подусловий и оба подусловия истинны (по критерию достаточно первых двух тестов). Для тестирования ветви «иначе» (в данном случае пустой) нужен один тест, где оба подусловия одновременно ложны.
Для сравнения критериев построим для примера из рис. 4.1 все возможные комбинации условий и построим соответствующие тесты (0 соответствует значению «ложь», 1 – «истина».
Подусловия 1-го усл.
Подусловия
2-го условия
1-е усл.
2-е усл.
Тест
Путь
A>1
B=0
X/A>1
A=2
X>1
(A>1)& (B=0)
(A=2)Ú
( X>1)
A
B
X
1
0
0
X не меняется, т.к. (A>1)& (B=0) ложь
0
0
0
0
1
1
1
abd
2
0
0
0
1
0
1
1
1
2
abe
3
0
0
1
0
0
1
Не м.б. A<=1 и A=2
4
0
0
1
1
0
1
Не м.б. A<=1 и A=2
5
0
1
0
0
0
0
1
0
1
abd
6
0
1
0
1
0
1
1
0
2
abe
7
0
1
1
0
0
1
Не м.б. A<=1 и A=2
8
0
1
1
1
0
1
Не м.б. A<=1 и A=2
9
1
0
0
0
0
0
2
1
1
abd
10
1
0
0
1
0
1
3
1
2
abe
11
1
0
1
0
0
1
2
1
1
abe
12
1
0
1
1
0
1
2
1
2
abe
13
1
1
0
0
0
1
0
3
0
1
acd
14
1
1
1
0
1
1
1
3
0
6
ace
15
1
1
0
1
0
1
1
2
0
1
ace
16
1
1
1
1
1
1
1
2
0
6
ace
Возможно, какие-то из приведенных тестов покрывают другие. 

Важно заметить, что даже при одном и том же итоговом числе тестов сложность их построения определяется как содержанием задачи, так и стилем записи алгоритма.
Так, для примера на рис. 4.1 сложность составления тестов определяется тем, что второе условие включает переменную X, которая в одних случаях меняется, а в других – нет. Если в условии учитывается измененное значение, то для теста надо восстановить исходное, т.е. подняться вверх по ветвям алгоритма. Вероятно, можно переписать этот фрагмент иначе, но представляется, что это не упростит дела.
Иная ситуация с примером нисходящей разработки из темы 3. Здесь при переходе к анализу очередного данного или к решению задачи в каждом условии явно представлены составляющие его подусловия.. Альтернативный вариант – фиксация верности/ошибочности группы проверенных данных с помощью одной логической переменной – даст простую запись условий (проверку только этой переменой), но эта переменная будет скрывать все предыдущие условия. В данном случае дело спасает нисходящая разработка с параллельным проектированием тестов. При необходимости построить тесты для уже готового алгоритма, использующего описанный прием, придется проходить вверх по ветвям, восстанавливая значения данных для теста.

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

Рис. 4.2. Тестирование программы по частям:
а) процесс преобразования входных данных;
б) формирование тестовых данных программы на основе тестовых данных фрагмента
Это весьма нетривиальная задача, не имеющая общего, тем более формального решения. Возникает она при независимом тестировании отдельных частей программы. Избежать этого процесса  (почти в полной мере) позволяет только нисходящий подход – отладка и тестирование, проводимые параллельно с проектированием. В этом случае на любом уровне тесты строятся для последовательности базовых конструкций или их вложения малой кратности (1 – 2). При необходимости построения дополнительных тестов при раскрытии абстракций задача обратного преобразования тестовых данных также значительно упрощается.

Построения тестов для базовых конструкций
Тестирование сколь угодно сложных конструкций сводится к тестированию базовых конструкций. Уточним требования к соответствующим тестам.
Следование. Вырожденный случай, никаких условий нет. Требуется по крайней мере один тест.
Развилка. Тест должен обеспечить выполнение каждого простого условия. Если условие содержит k простых подусловий, то общее число возможных комбинаций значений подусловий 2. Реально из этих комбинаций выбрасываются невыполнимые или неинформативные. Так, например, в последнем ветвлении алгоритма из примера нисходящей разработки содержатся 4 подусловия:




1.1, 1.2 (для ветви то)

2.1, 2.2, 3.1, 4.1, 4.2, 5.1 (для ветви иначе)
 если (nver & mver & bver & aver) {все данные верны} то
     {решение задачи} {вх: n,b,m,a; вых.: a или maxmin
      A0.3. <решение задачи с выводом результатов по обр 3 > 
  иначе {хотя бы одно данное неверно, Ønver Ú Ømver Ú Øbver Ú Øaver }             
      вывод по обр.4.5;    
  кесли;

Формально можно построить 16 их комбинаций, однако некоторые из соответствующих тестов покрываются другими:


nver
mver
bver
aver
Тестируемая ветвь
Комментарий
1
0
0
0
0
иначе
Неинформативны
2
0
0
0
1
иначе
3
0
0
1
0
иначе
4
0
0
1
1
иначе
5
0
1
0
0
иначе
6
0
1
0
1
иначе
7
0
1
1
0
иначе
8
0
1
1
1
иначе
Тесты 2.1, 2.2
9
1
0
0
0
иначе
Неинформативны
10
1
0
0
1
иначе
11
1
0
1
0
иначе
12
1
0
1
1
иначе
Тесты  4.1, 4.2
13
1
1
0
0
иначе
Неинформативен
14
1
1
0
1
иначе
Тест 3.1
15
1
1
1
0
иначе
Тест 5.1
16
1
1
1
1
то
Тесты 1.1, 1.2



Цикл. Тест должен обеспечивать по крайней мере однократное прохождение цикла и выход из него.
Если условие цикла состоит из нескольких подусловий, то требуется обеспечить выполнение каждого подусловия, как и в случае ветвления.
Основой конструирования тестов является построение таблицы истинности отдельных подусловий и условия в целом. Схема конструирования с примерами приведена в файле test_bas.doc.

Построения тестов для вложенных конструкций
При нисходящем подходе любая конструкция любой глубины вложенности является результатом поэтапного раскрытия абстракций. Поэтому в качестве базы для тестирования вложенных конструкций можно рассматривать однократное вложение (т.е. полагать, что тестируется программа соответствующего уровня).
Будем при этом говорить о вложенной и объемлющей конструкции.
Любая вложенная конструкция в рамках программы выполняется только на том множестве тестов, на котором выполняется объемлющая ее конструкция. Поэтому, если такое множество тестов имеется, прежде всего необходимо выбрать нужное подмножество и проверить, является ли оно достаточным для тестирования вложенной конструкции.
Так, в примере нисходящей разработки из темы 3 для тестирования развилки, вложенной в развилку, на уровне 0 и развилки, вложенной в цикл, на уровне 1 оказывается достаточно подмножества функциональных тестов:

1.1, 1.2, 4.1, 4.2, 5.1 (для всей развилки)


4.1,4.2 (для вложенной развилки)
          если (nver & bver){предыдущие данные верны} то
               ввод(m); вывод(m,n)по обр 2.3;                
               {проверка m} 
           mver:=истина; 
                если (m<=0 или m>50) то {m неверно}
                     mver:=ложь; вывод по обр 4.3;                   
                кесли;
           кесли;


Тесты для цикла:
1.1, 1.2, 3.1, 4.1, 4.2, 5.1

Для развилки: 3.1
    {проверка массива b}
    bver:=истина;
    для i от 1 до n цикл          
        если |b(i)| >10 то
           вывод (i,b(i)) по обр 4.2;
               bver:=ложь;
        кесли;
    кц;                       

В то же время при раскрытии абстракции A0.3 (решении собственно задачи) выявляется необходимость анализа наличия строки матрицы со всеми положительными элементами. Следовательно, необходимы тесты  для соответствующей ветви. Реально эта необходимость была обнаружена автором только после построения алгоритма A0.3 и составлении структурных тестов для него. Соответствующий тест был составлен и добавлен в общий набор тестов. Эта часть разработки пока не выдана.





[1] Р. Гласс.  Руководство по надежному программированию. – М., Финансы и статистика, 1982. – 256 с.
[2] Здесь видна связь структурного и функционального тестирования, поскольку набор операторов – это элемент структуры программы, которая, по определению функционального тестирования, игнорируется.
[3] Г. Майерс. Искусство тестирования программ. – М., Финансы и статистика, 1982. – 176 с. 

Комментариев нет:

Отправить комментарий