6. Тестирование циклов

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

Схемы на рисунке изображают устройство циклов с предусловием и с постусловием. Циклы for можно считать частным случаем циклов с предусловием.

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

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

НЕ нашли? Не то? Что вы ищете?

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

1. Выбирается самый внутренний цикл. Устанавливаются минимальные значения параметров всех остальных циклов.

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

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

4. Работа продолжается до тех пор, пока не будут протестированы все циклы.

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

- список управляющих вопросов;

- у каждого из выбранных управляющих вопросов - список рангов;

- каждому управляющему вопросу и выбранному рангу – список пунктов;

Эти пункты в совокупности составляют множество влияющих пунктов.

Аналогично выбираются:

- список подчиненных вопросов;

- у каждого из выбранных подчиненных вопросов - список рангов;

- каждому подчиненному вопросу и выбранному рангу – список пунктов;

Эти пункты в совокупности составляют множество влияемых пунктов.

В тестируемом модуле вычисляются и выдаются коэффициенты влияния каждого влияющего пункта на каждый влияемый. Функция, дающая этот коэффициент, зависит от шести параметров: NW1, NG1, NA1, NW2, NG2, NA2. к тому же группировать результат надо по группам. Поэтому естественно организовать всю процедуру в виде вложенных циклов глубины 6:

for(NW1=1; NW1<=CountW1; NW1++)

for(NG1=1; NG1<=CountG1; NG1++)

for(NA1=1; NA1<=CountA1; NA1++)

for(NW2=1; NW2<=CountW2; NW2++)

for(NG2=1; NG2<=CountG2; NG2++)

for(NA2=1; NA2<=CountA2; NA2++)

{

Вычисление_и_выдача(NW1, NG1, NA1, NW2, NG2, NA2);

}

Если следовать указанной выше методике тестирования, то для проверки этого блока:

Устанавливаем NW1, NG1, NA1, NW2, NG2 равными 1 и гоняем программу с разными параметрами по самому внутреннему циклу

for(NA2=1; NA2<=CountA2; NA2++)

{

Вычисление_и_выдача(1, 1, 1, 1, 1, NA2);

}

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

Теперь переходим к следующему по вложенности изнутри циклу. Оставляя NW1, NG1, NA1, NW2 равными 1, прогоняем

for(NG2=1; NG2<=CountG2; NG2++)

for(NA2=1; NA2<=CountA2; NA2++)

{

Вычисление_и_выдача(1, 1, 1, 1, NG2, NA2);

}

При этом берем не все тесты предудущей проверки, а что-то одно, касаемое пунктов NA2. Файл выдачи значительно увеличивается. Если его результаты зависят от установок для влияемых групп (длинные/короткие имена и т. д.), то тестируем эти варианты. В этом духе продолжаем, пока не выберемся из самого внешнего цикла.

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

for(NW1=1; NW1<=CountW1; NW1++)

{

CountG1=Подготовка1(NW1);

for(NG1=1; NG1<=CountG1; NG1++)

{

CountA1=Подготовка2(NW1,NG1);

for(NA1=1; NA1<=CountA1; NA1++)

{

CountW2=Подготовка3(NW1,NG1,NA1);

for(NW2=1; NW2<=CountW2; NW2++)

{

CountG2=Подготовка4(NW1,NG1,NA1,NW2);

for(NG2=1; NG2<=CountG2; NG2++)

{

CountA2=Подготовка5(NW1,NG1,NA1,NW2);

for(NA2=1; NA2<=CountA2; NA2++)

{

Вычисление_и_выдача(NW1, NG1, NA1, NW2, NG2, NA2);

}

}

}

}

}

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

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

Неструктурированные циклы. Неструктурированные циклы тестированию не подлежат.

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

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

· предусловие было бы ложно с самого начала, т. е. входа в цикл вообще бы не происходило;

· цикл выполнялся бы только один раз;

· цикл выполнялся бы максимальное число раз.

То же самое для цикла for earch.

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

Пример.

while not eof(F) do

begin

readln(F, S);

if S[1]=’*’ then break;

end;

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

7. Отладка программного обеспечения

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

Из всех этапов проектирования логики программных модулей этап отладки является наиболее сложным и менее формализованным.

Этапы общей методики отладки

1 этап - изучение проявления ошибки.

2 этап - локализация ошибки.

3 этап - определение причины ошибки.

4 этап - исправление ошибки.

5 этап - повторное тестирование - повторение всех тестов с начала.

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

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

Ошибки выполнения относятся к самой непредсказуемой группе. Выделяют четыре способа проявления таких ошибок:

· сообщение об ошибке системы контроля выполнения машинных команд (переполнение, деление на ноль, нарушение адресации и т. п.);

· сообщение об ошибке операционной системы (нарушении защиты памяти, попытке записи на устройства, защищенные от записи, отсутствии файла с заданным именем и т. п.);

· «зависание» компьютера;

· несовпадение полученных результатов с ожидаемыми.

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

Причины ошибок можно разделить на следующие группы:

· неверное определение исходных данных;

· логические ошибки;

· накопление погрешностей результатов вычислений.

Неверное определение исходных данных происходит возникает при выполнении операциях ввода-вывода: ошибки передачи, ошибки преобразования, ошибки перезаписи и ошибки данных.

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

К последней группе относят:

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

· использование переменных до их инициализации;

· использование индексов, выходящих за границы определения массивов;

· нарушения соответствия типов данных при их переопределении (char *b[]=”Ф”; unsigned int a = (unsigned int)(b[0]););

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

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

· другие.

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

Cложность отладки увеличивается также вследствие влияния следующих факторов:

· опосредованного проявления ошибок;

· возможности взаимовлияния ошибок;

· возможности получения внешне одинаковых проявлений разных ошибок;

· отсутствия повторяемости проявлений некоторых ошибок от запуска к запуску – так называемые стохастические ошибки;

· возможности изменения внешних проявлений ошибок при внесении изменений в программу;

· написания отдельных частей программы разными программистами.

Методы отладки. Большинство ошибок (может быть около 95%) при отладке авторами программ находится достаточно быстро и легко исправляется. Правда замечено, что у слишком “быстро соображающих” программистов с первого раза исправления никогда не получаются. Всегда надо делать несколько проверок и исправлений одного и того же места, и уверенность в том, что ошибка исправлена окончательно, невелика. Поэтому даже при исправлении очевидных логических ошибок лучше не торопиться.

Трудные ошибки составляют небольшую в процентном отношении часть. Но именно они забирают основную массу времени и сил.

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

· метод ручного тестирования;

· метод обратного прослеживания.

· метод индукции;

· метод дедукции;

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

Обратное прослеживание. Естественный и простой метод при работе с пошаговым отладчиком. В большинстве случаев нетрудно найти точку в программе, в которой программа уже сделала то, чего быть не должно. Результат обычно наблюдается как неправильные значения каких-то переменных. Задача в том, чтобы “отматывая кадры назад”, найти первое место, где произошел поворот на неправильную дорогу. Прослеживаем пути, по которым вычисление могло прийти к уже известному неправильному месту и расставляем там точки останова. Повторное выполнение теста состановится на одной из этих точек. Анализируем, произошел сбой или еще нет. В Зависимости от этого или опять откатываемся назад или, имеем ветку, где произошел сбой и следующую точку останова вставляем где-то в ее середине. Продолжаем в том же духе, используя по существу способ дихотомии (деления пополам). Процесс продолжают, пока ошибка не локализуется. Тут же обнаружится и причина – чаще всего в виде какой-то дурацкой опечатки. Чтобы использовать этот метод, надо уметь в каждой точке определять, произошел сбой или еще нет. В самых тяжелых случаях именно это и не удается. Тогда-то отладка становится очень трудной или вовсе невозможной.

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

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

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

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

· отладочный вывод;

· интегрированные средства отладки;

· независимые отладчики.

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

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

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

Интегрированные средства отладки. Большинство современных сред программирования (Delphi, Builder C++, Visual Studio и т. д.) включают средства отладки, которые обеспечивают максимально эффективную отладку. Они позволяют:

· выполнять программу по шагам, причем как с заходом в подпрограммы, так и выполняя их целиком;

· предусматривать точки останова;

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

· отображать содержимое любых переменных при пошаговом выполнении;

· отслеживать поток сообщений и т. п.

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

Бесплатные советы.

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

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

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

· обратить особое внимание на корректное распределение памяти под данные.