Автор Анна Евкова
Преподаватель который помогает студентам и школьникам в учёбе.

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

Содержание:

ВВЕДЕНИЕ

Главная цель программирования – это написание программы, но к сожалению, в большинстве случаев первый запуск приводит к ошибке. Какие же бывают ошибки?

Во-первых, программа может содержать синтаксические ошибки.

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

И в-третьих программа может неправильно реагировать на ввод некорректных данных.

Каким же образом нам найти ошибки в программе?

В этом поможет тестирование и отладка.

Отладка - это определение места ошибки и её исправление с использованием процессов выполнения его программ.

Тестирование - это процесс выполнения программ на некотором наборе данных, для которого заранее известен результат применения или известны правила поведения этих программ.

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

Для уменьшения количества тестов необходимо распределить на группы все возможные варианты выполнения программы. Существует два подхода: подход «черного ящика» и «белого ящика». Критерии черного ящика не учитывают внутреннее устройство программы. Критерии белого ящика учитывают.

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

Глава 1. МЕТОДЫ ТЕСТИРОВАНИЯ

1.1 Критерии черного ящика

Существуют следующие критерии черного ящика:

1) тестирование функций;

2) тестирование классов входных данных;

3) тестирование классов выходных данных;

4) тестирование области допустимых значений (тестирование границ класса);

5) тестирование длины набора данных;

6) тестирование упорядоченности набора данных.

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

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

Критерий тестирования классов выходных данных выглядит аналогично предыдущему критерию, только проверяются не входные данные, а выходные.

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

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

Если класс допустимых значений представляет собой числовой диапазон, то понадобится более серьезная проверка. В этом случае выделяются:

1) нормальные условия (в середине класса);

2) граничные (экстремальные) условия;

3) исключительные условия (выход за границу класса).

Например, пусть программа предназначена для обработки деканатом информации об одной студенческой группе. Нормальное число студентов в группе — 20–25. Но на младших курсах их часто бывает больше, а на старших — наоборот. Пусть максимальное число студентов в группе ограничено 30. В этом случае имеет смысл проверить правильность работы программы с группой из 20 студентов (нормальные условия), с группой из 30 человек и с группой из одного человека (экстремальные условия). Необходимо проверить, как программа отреагирует на приказ о зачислении в группу 31-го студента и об отчислении последнего остававшегося в группе студента (исключительные условия).

Иногда требуется более тонкая градация. Возможна ситуация, когда вся область допустимых значений делится на отдельные подобласти, требующие от программы разной реакции. Например, пусть программа готовит сводный отчет о работе некоторой фирмы. Известно, что ежедневно фирма продает продукции примерно на 30 тыс. руб. Как должна реагировать программа на сообщение о том, что доход за день 10 руб. или 1 000 000руб.? Теоретически можно допустить, что этот фирма сработала настолько плохо или, наоборот, потрясающе. Однако логично предположить, что в этих случаях при вводе данных возникли проблемы или данные введены не верно? В результате вся область допустимых значений делится на 3 подобласти: подобласть нормальных значений, подобласть подозрительно больших значений и подобласть подозрительно маленьких значений. Нормальные данные программа должна просто обрабатывать. Подозрительно большие и подозрительно малые значения допустимы, однако при их получении имеет смысл затребовать подтверждения, действительно ли все верно введено.

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

Следующие два критерия являются частными случаями, уточняющими ранее названные критерии. Но в силу их важности и частой применимости, они заслуживают отдельного упоминания.

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

1) пустой набор (не содержит ни одного элемента);

2) единичный набор (состоит из одного-единственного элемента);

3) слишком короткий набор (если предусмотрена минимально допустимая длина);

4) набор минимально возможной длины (если предусмотрено);

5) нормальный набор (состоит из нескольких элементов);

6) набор из нескольких частей (если такое возможно. Например, если программа читает литеры из текстового файла или печатает текст, то как она отнесется к переходу на следующую строку? На следующую страницу?);

7) набор максимально возможной длины (если предусмотрено);

8) слишком длинный набор (с длиной больше максимально допустимой).

Тестирование упорядоченности входных данных важно для задач сортировки и поиска экстремумов. В этом случае имеет смысл проверить следующие ситуации (классы входных данных):

1) данные неупорядочены;

2) данные упорядочены в прямом порядке;

3) данные упорядочены в обратном порядке;

4) в наборе имеются повторяющиеся значения;

5) экстремальное значение находится в середине набора;

6) экстремальное значение находится в начале набора;

7) экстремальное значение находится в конце набора;

8) в наборе несколько совпадающих экстремальных значений.

1.2 Критерии белого ящика

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

В качестве примера рассмотрим следующий фрагмент Java программы:

Пример 1

a = 0;

if (x>3) {a = 10;}

b =1/a;

Для того чтобы удовлетворить критерию покрытия операторов, достаточно одного выполнения. Такого, чтобы x был больше 3. Очевидно, что ошибка в программе этим тестом обнаружена, не будет. Она проявится как раз в том случае, когда x <= 3. Но такого теста критерий покрытия операторов от нас не требует.

Итак, мы имеем программу, оттестированную с точки зрения критерия покрытия операторов и при этом содержащую ошибку.

Следуя критерию покрытия операторов, мы проверили только положительную ветвь развилки, но не затронули отрицательную.

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

Рассмотрим другой пример. На Java он будет выглядеть

так:

Пример 2

a = 7;

while (a>x) { a--;}

b = 1/a;

Для того чтобы удовлетворить критерию покрытия ветвей, в данном случае достаточно одного теста. Например, чтобы x был равен 6 или 5. Все ветви программы будут пройдены (при x = 5 одна из ветвей — тело цикла — даже 2 раза). Но ошибка в программе обнаружена так и не будет! Она проявится в одном-единственном случае, когда x = 0. Но такого теста от

нас критерий покрытия ветвей не потребовал.

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

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

покрытие путей.

Кроме проблем с проверкой циклов существенные проблемы связаны с проверкой сложных условий — логических выражений, содержащих знаки дизъюнкции и/или конъюнкции.

Например:

if (a<b || c=0) {..}

while (i<=n && x>exp) {..}

И в том, и в другом операторе можно пройти по обеим ветвям, изменяя значение только одного из простых условий. Пусть c не равно 0. Меняя значение переменных a и b, можно пройти и по ветви «то», и по ветви «иначе». При этом ситуация, когда c = 0, останется непроверенной. Аналогично, пусть i <= n. Меняя значения переменных x и eps, можно управлять выполнением цикла while, не проверив поведение программы при i > n. Для того чтобы учесть подобные ситуации, были предложены следующие критерии:

-критерий покрытия условий;

-критерий покрытия решений/условий;

-критерий комбинаторного покрытия условий.

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

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

Например:

If (a>0 || c=0) { d=1;} else { d= 1/c;}

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

Чтобы исправить этот недостаток, критерии покрытия ветвей (решений) и условий объединяют в единый критерий покрытия решений/условий. Он требует набор тестов, при котом каждая ветвь в программе была пройдена хотя бы один раз и чтобы каждое простое условие (слагаемое в дизъюнкции и сомножитель в конъюнкции) получило и значение «истина», и значение «ложь» хотя бы один раз. Критерий надежнее, чем простое покрытие ветвей, но сохраняет его принципиальный недостаток: плохую проверку циклов. Приведенный выше пример 2, «ломающий» критерий покрытия ветвей, «сломает» и критерий покрытия решений/условий. Ошибка в данном случае проявится только при фиксированном количестве повторений цикла (в примере 2 — семикратном), а критерий покрытия решений/условий не гарантирует, что повторений будет именно столько.

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

Пример 3

if (a=0 || b=0 || c=0) { d= 1/(a+b); }

else {d = 1;}

В примере 3 шибка будет выявлена только при одновременном равенстве нулю двух переменных: a и b. Критерий покрытия решений/условий не гарантирует проверки такой ситуации.

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

1.3 Минимальное грубое тестирование

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

1) для каждого цикла с предусловием должна быть проверена

правильность при кратном нулю, однократном и многократном повторении тела цикла. Многократным считается любое повторение более одного раза;

2) для каждого цикла с постусловием должна быть проверена

правильность при однократном и многократном повторении

тела цикла;

3) проверка цикла со счетчиком зависит от того, фиксированы

ли границы изменения счетчика или вычисляются. Вообще

говоря, как и для цикла с предусловием, требуется проверка

при нулькратном, одно- и многократном повторении тела

цикла.

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

Строки таблицы соответствуют проверяемым условиям, графы — тестам. Для каждого условного оператора в таблице МГТ создаются 2 строки: для ветви «то» и ветви «иначе».

Пример таблица 1:

Таблица 1

Тест 1

Тест 2

Тест 3

Тест 4

Тест 5

If a>b

+

-

Для каждого цикла с предусловием — 3 строки: для кратного нулю, однократного и многократного повторения тела цикла. Пример таблица 2:

Таблица 2

Тест 1

Тест 2

Тест 3

Тест 4

Тест 5

While a>b

=0

=1

>1

Для каждого цикла со счетчиком — 3 строки: для кратного нулю, однократного и многократного повторения тела цикла.

В случае разбиения программы на процедуры, модули и т. п. МГТ-таблицы строятся для каждой процедуры, модуля и пр. Соответственно, тестирование проводится прицельно для данной процедуры, для данного модуля.

Критерий МГТ не идеален, как и все предыдущие. Он не гарантирует проверку комбинации всех простых условий. Он не гарантирует надежную проверку циклов. Приведенный в предыдущей части пример 3, который «ломал» критерий решений/условий, «сломает» и критерий МГТ. Выполнение требований минимально грубого тестирования не гарантирует проверку всех возможных комбинаций простых условий. Ошибка в примере 2, выявление которой не гарантировали критерии покрытия ветвей, решений/условий и комбинаторного покрытия условий, может быть пропущена и критерием МГТ. Тем не менее, критерий МГТ представляется достаточно разумным компромиссом между надежностью и сложностью тестирования.

Глава 2. ОШИБКООПАСНЫЕ СИТУАЦИИ

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

2.1 Обращение к данным

1. Использование значения переменной.

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

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

2. Автоматическая инициализация переменных.

Опасность: Неверная инициализация.

Некоторые системы программирования автоматически заполняют выделяемую память стандартными значениями, чаще всего нулями. Всегда или при включении соответствующего режима. Проверьте, делает ли это ваша система. Использует ли она нужное вам значение? Можно ли управлять процессом инициализации?

  1. Индексация массива.

Опасность: Выход индекса за границу измерения.

  1. Изменение переменной внутри блока.

Опасность: Побочный эффект (изменение глобальной переменной при выполнении подпрограммы).

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

  1. Использование значения ссылочной переменной.

Опасность: «Висячая ссылка».

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

  1. Схожие имена переменных.

Само по себе это не ошибка. Но может быть причиной описки. Соответственно требует повышенного внимания.

  1. Использование записи с вариантами.

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

Само по себе это не ошибка. Но может оказаться, что эти поля имеют разные типы. В этом случае величина, записанная как значение одного типа, может быть прочитана или откорректирована как значение другого типа.

  1. Использование не типизированного указателя.

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

9. Использование переменных, не имеющих явного описания.

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

2.2 Вычисления

1. Выражение

Опасность: Неверный порядок вычисления операций в выражении.

Пример:

int a =2;

int v = ++a + ++a * ++a;

Математик будет ожидать умножения, а потом сложения. Хотя умножение выполняется перед сложением, первыми вычисляются операнды оператора ++. Таким образом, выражение равно 3+4*5, или 23.

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

  1. Логическое выражение

Опасность: Путаница между полной и краткой схемами вычисления логических выражений.

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

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

  1. Сравнения > / <

Опасность: Потеря третьего результата операции сравнения.

В математике сравнение двух чисел может иметь 3 исхода: больше, меньше, равно. В языках программирования операции сравнения — логические, т. е. имеющие только 2 исхода. Третий исход зачастую оказывается потерянным. Для «полноценного» сравнения требуется не один условный оператор, а два вложенных:

if (a>b) {…} else { if (a=b) { …} else {a<b} …}

  1. Сравнения> и >= , < и <=

Опасность: Ошибки типа «±1».

Частая ошибка — путаница сравнений на строгое и нестрогое неравенство: > и >=, < и <=. Если сравнение стоит в заголовке цикла, то он будет повторяться на один раз больше или меньше требуемого при неправильном выборе знака сравнения. Отсюда и название типа ошибки.

  1. Деление

Опасность: Деление на нуль.

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

  1. Извлечение квадратного корня

Опасность: Корень из отрицательного числа.

Аргумент не должен получить недопустимое значение.

  1. Взятие логарифма

Опасность: Логарифм неположительного числа.

Убедитесь, что аргумент не может получить недопустимое значение.

  1. Использование в вычислениях данных «не того» типа

Опасность: Неверное приведение типов данных.

Например, использование литерных значений в арифметических операциях или целочисленных значений — в логических. Строго типизированные языки такое запрещают. Но не все языки строго типизированы. В слабо типизированных языках это не обязательно ошибка. Но имеет смысл уточнить, как именно будут выполняться операции. Например, какой результат даст сумма 1 + ‘1’? Чему будет равна литерная единица:

целочисленной единице или коду литеры ‘1’?

  1. Присваивание целой переменной вещественного значения

Опасность: Способ преобразования вещественного числа в целое.

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

  1. Вычисления с плавающей точкой (вещественная арифметика)

Опасность 1: Погрешности округления.

Машинные вычисления не всегда совпадают с арифметическими. В первую очередь это относится к вещественным значениям. Причин тому две. Во-первых, в машинной памяти числа хранятся не в десятичном формате, а в двоичном. Преобразование выполняется с некоторым округлением. Во-вторых, точность представления чисел в памяти ограничена. В результате вещественная единица вполне может оказаться равна 0,9999999999, а может - 1,0000000001 (количество знаков после запятой зависит от точности представления).

Пример:

a= 1/3;

b= a*3;

В математике b будет равно 1. В программировании для начала окажется

а=1/4+1/16+1/64+1/256 и т. д. в зависимости от точности представления. Это не совсем одна треть. В конце концов, наберется сумма, которая с заданной точностью даст a = 0,3333333333.Но тогда b = 0,9999999999, а не 1. Если таких значений в выражении будет несколько или выражение многократно пере вычисляется в цикле, ошибка округления будет копиться. К счастью, в большинстве языков счетчик цикла не может быть вещественным. Но поскольку в таких конструкциях есть объективная необходимость, его приходится моделировать.

Опасность 2: Потеря значимости (получение чисел с очень маленькой мантиссой).

Слишком маленькая мантисса в машинной арифметике может превратиться в нуль.

  1. Сравнение вещественных чисел

Опасность: Погрешности округления.

Погрешности округления чреваты ошибками не только в арифметических операциях (о чем уже было сказано). В операциях сравнения они могут привести к тому, что «лобовое сравнение» вещественных чисел даст неверный результат. Пусть вещественные переменные q и r обе равны единице. Но в одном случае вещественная единица будет представлена как 0,9999999999, а в другом — как 1,0000000001. В этом случае сравнение q r вполне может дать значение «ложь». Лучше сравнивать модуль разности с некоторым eps.

  1. Арифметические вычисления (как вещественные, так и целые)

Опасность 1: Переполнение (получение очень больших чисел).

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

Опасность 2: Переполнение или потеря значимости в промежуточных вычислениях.

Конечный результат может иметь нормальный вид, но промежуточные могут оказаться слишком большими или слишком маленькими. Естественно, о правильности конечного результата в данном случае говорить не приходится. Для машинной арифметики порядок выполнения операций может оказаться весьма существенным. Это в математике a*b/c=a/c*b. В программировании - не всегда.

2.3 Передача управления

1. Развилки

Опасность: Пропущена ветвь «иначе».

Этот момент особенно важен для оператора выбора. Что будет делать ваша программа, если значение выражения после case не соответствует ни одному из перечисленных вариантов?

  1. Вложенные условные операторы

Одинаковая запись в разных языках программирования трактуется по-разному.

  1. Циклы

Опасность: Зацикливание.

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

2.4 Подпрограммы

1. Вызов подпрограммы

Опасность 1: Неверное количество параметров.

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

Опасность 2: Неверные типы параметров.

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

Опасность 3: Неверный порядок следования параметров.

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

2. Использование параметров внутри подпрограммы

Опасность: Неверные единицы измерения параметров.

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

Использование формального параметра внутри подпрограммы для изменения значения соответствующего фактического параметра

Опасность: Неверный способ передачи параметров.

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

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

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

К счастью, в большинстве языков такое запрещено.

  1. Использование внутри подпрограммы формальных параметров, передаваемых по ссылке

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

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

  1. Использование внутри подпрограммы глобальной переменной и параметра, передаваемого по ссылке

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

2.5 Файлы

1. Запись/чтение двоичных файлов

Опасность: Путаница между файлами разных типов.

  1. Создание файлов с данными с помощью текстового редактора. Просмотр файлов с данными с помощью текстового редактора

Опасность: Путаница текстовых и двоичных файлов.

  1. Запись/чтение не типизированных файлов

Опасность: Еще одна возможность обойти контроль типов. Данные могут быть записаны в файл как значения одного типа, а прочитаны как значения другого.

  1. Запись в файл

Опасность: Отсутствие явного закрытия файлов.

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

Глава 3. ОТЛАДКА

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

3.1 Место проявления ошибки и место нахождения ошибки

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

a= 0;

if (x>3) {a= 10;}

b= 1/a;

Место проявления ошибки в данном случае — третий оператор фрагмента:

b=1/a. Но место нахождения ошибки указать так определенно нельзя. Возможно, что ошибка в третьем операторе, и в знаменателе должна стоять не переменная a, а какое-то другое выражение. Возможно, что ошибка в первом операторе, и переменной a должно было быть присвоено другое ненулевое значение. Возможно, что ошибка во втором операторе, и там потеряна ветвь «иначе», при выполнении которой переменная a должна была поменять свое значение на значение не равное нулю.

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

3.2 Методы поиска ошибки

3.2.1 Индуктивный и дедуктивный

Для поиска ошибок существует два подхода: индуктивный и дедуктивный.

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

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

основания подозревать ошибку.

Дедуктивный подход означает движение от общего к частному. Что в принципе могло произойти? Исходя из соображений, формируется множество гипотез. Затем оно уточняется: какие-то гипотезы исключаются как несоответствующие имеющимся признакам ошибки, какие-то уточняются за счет дополнительной информации. Например: программа нормально заканчивает вычисления, но выдает странные результаты. На выходе ожидались два числа, близких к единице с одним знаком после запятой. Получили одно число очень большое, а второе — со странной непериодической дробной частью. Известно, что «странные» выходные данные могут получиться в нескольких случаях. Либо если в вычислениях участвовала неинициализированная переменная, либо при вводе из файла «не тех» значений (например, файл готовился как текстовый, а читался как типизированный), либо при неверной передаче значения параметра из подпрограммы, либо при неправильных вычислениях (например, требующих слишком больших или слишком маленьких промежуточных результатов). Из этого следует что, надо проверить, все ли переменные, использованные при вычислении «странного» результата, были явно инициализированы. Не использовались ли значения, которые введенные из файла? Если да, то что за данные лежат в этом файле. Не было ли передачи параметра из подпрограммы? Если да, то передается ли этот параметр по ссылке? Результатом, каких именно вычислений являются «странные» значения? Не возникли ли по ходу этих вычислений слишком больших или слишком маленьких промежуточных результатов?

3.2.2 Ретроанализ

Раньше в шахматных журналах были очень популярны задачи на ретроанализ. Выглядели они так. Дается ситуация, при которой белые ставят мат в один ход и черные ставят мат в один ход. Вопрос: кто выиграет? То есть надо разобраться, чей сейчас ход. А для этого «открутить» игру назад и понять, каким образом сложилась описанная в задаче ситуация. Как ретроанализ выглядит применительно к отладке? Находим место ошибки. Ошибка связана с тем, что некоторые переменные имеют определенные значения. Проследим назад по программе, откуда эти значения взялись. Они были вычислены через некоторые другие переменные. А откуда взялись значения этих других переменных? И так далее.

Пример:

int a = in.nextInt();

b= 7;

c= a + b;

d= 1/c;

При вычислении d возникает деление на нуль. Значит, переменная c в этом операторе равна нулю. Возможны два варианта. Либо ошибочно выражение, записанное в знаменателе (знаменатель должен быть отличен от c), либо переменная c имеет неверное значение. Если выражение в знаменателе правильно, значит, переменная c имеет неверное значение. Откуда взялось значение переменной c? Поднимаемся вверх по программе. Переменная c получила значение из выражения a+b. Значит, либо в этой позиции должно стоять другое выражение, либо что-то не так со значениями переменных a и b. Откуда взялись значения этих переменных? Опять поднимаемся вверх по программе. И так далее. Не всегда картина бывает такой ясной. Разбираться приходится и с развилками, и с циклами, и с подпрограммами. Но принципиальная схема именно такова.

3.3 Принципы отладки

1. Не экспериментировать. Исправление ошибок наугад не допустимо.

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

3. Необходимо найти ошибку, которая бы объясняла все 100% симптомов.

4. Там, где есть ошибка, может быть еще.

5. Исправление может внести новую ошибку.

3.4 Анализ ошибок

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

1. Когда была сделана ошибка?

На каком этапе работы над программой допущена ошибка: при постановке задачи, при проектировании программы, при написании текста на языке программирования и т. д.

2. Почему была сделана ошибка?

Что именно было непонятно? Была ли причиной ошибки неточность в формулировке задачи, или недопонимание какой-то языковой конструкции, или неудачный выбор имени переменной (который сделал описку ошибкой), или что-либо еще.

3. Как можно было предотвратить ошибку?

Есть хороший методический принцип: «Никогда не спрашивай человека, чему он научился. Спрашивай, что он в следующий раз будет делать по-другому». Именно этот вопрос мы сейчас и задаем. Что в следующий раз надо делать по-другому, чтобы подобные ошибки не повторялись?

  1. Почему ошибку не обнаружили раньше?

Если ошибку удалось обнаружить сейчас, то почему ее не удалось обнаружить раньше, на более ранних этапах?

  1. Как можно было обнаружить раньше?

Как нашли ошибку? Почему именно этот тест оказался удачным? Нельзя ли таким же образом найти аналогичные ошибки в других программах?

3.5 Тесты для полной проверки программы

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

1. Тестирование функциональности

2. Тестирование удобства эксплуатации

3. Тестирование на предельных объемах

Проверить, как программа работает с очень большими объемами данных. Например, если вы пишете редактор, загрузите в него очень большой документ. Если программа нумерует кадры в электронной фотокамере и под номер кадра отведено 4 позиции, попробуйте сделать более 10 000 снимков. Если ваша программа руководит доставкой товара покупателю, сделайте такую покупку, которая не поместится в одном грузовике.

4. Тестирование на предельных нагрузках

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

5. Тестирование защиты

6. Тестирование производительности

Докажите, что программа не обеспечивает требуемого времени отклика, пропускной способности и другое.

7. Тестирование требований к памяти

Найдите условия, при которых программа превысит допустимый расход оперативной или дисковой памяти, объем буферов и т. п.

8. Тестирование конфигураций

Способна ли программа работать на разных платформах, под управлением разных операционных систем и т. п.?

9. Тестирование совместимости

Совместима ли новая версия программы с предыдущей версией или с той программой, которую она заменяет?

10. Тестирование удобства установки (настройки)

11. Тестирование надежности

Сюда относится:

- проверка возможности обнаруживать ошибки пользователя;

- проверка возможности обнаруживать сбои аппаратуры;

- проверка возможности восстанавливать работоспособность системы после ошибок пользователя и сбоев аппаратуры;

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

- проверка среднего времени восстановления системы после отказа;

- проверка количества ошибок в программе;

- проверка последствий отказов системы;

- проверка допустимости объема данных, теряемых в случае отказа;

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

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

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

- наличие и работоспособность функций, необходимых для обеспечения устойчивости к ошибкам.

12. Тестирование удобства обслуживания

Сложная программа часто требует определенных действий по поддержанию своей работоспособности. Эти действия необходимо проверить.

13. Тестирование документации. Насколько она понятна, точна, однозначна и пр.

14. Тестирование процедур, которые должны выполняться человеком

ЗАКЛЮЧЕНИЕ

СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ

  1. М. Плаксин Тестирование и отладка программ для профессионалов будущих и настоящих.
  2. Гленфорд Майерс, Том Баджетт, Кори Сандлер Искуство тестирования программ.