Программа дает правдоподобные результаты 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Программа дает правдоподобные результаты



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

Как убедиться в том, что результаты правильны, а не правдоподобны? Здесь помогает тестирование программы. Тщательно подобранные тесты позволят отличить правдоподобные значения от правильных результатов. Как прави­ло, одного набора тестов недостаточно для убедительного доказательства неверности работы программы. Специалисты-тестировщики всегда подби­рают несколько наборов тестов так, чтобы точнее выявить дефекты про­граммы.

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

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

Локализация ошибки

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

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

Когда и прокрутка не помогает, приходится проверять логику выполнения программы. Проверка начинается с места обнаружения ошибки. Просмат­ривается полученная в этой точке отладочная печать, и проводятся рассуж­дения вроде следующих: "Такие результаты мог дать оператор А или опера­тор В. Но оператор В в этом случае не может выполняться. Значит, посмотрим на оператор А. Оператор А дал значение С, а оно могло полу­читься только в ситуации D. Чтобы возникла ситуация D, надо, чтобы...". На каждом логическом шаге приходится делать отладочную печать и не­большую прокрутку. Привлечение логических рассуждений помогает быст­рее проделать эту трудную работу, поскольку концентрирует внимание только на тех переменных, которые могут получить ошибочные значения, а таких переменных немного.

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

Устранение ошибки

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

Другие ошибки, например, те, которые возникают из-за неправильно запро­граммированного алгоритма, исправить сложнее. Их устранение требует из­менения больших кусков программного кода. Такие изменения зачастую вносят в программу новые ошибки. Эти ошибки снова надо обнаруживать, локализовать и устранять. Процесс отладки становится циклическим. Его приходится повторять несколько раз. Как гласит шутливая аксиома отладки: "Каждая последняя ошибка является предпоследней".

Чтобы не попасть в неприятный цикл отладки или быстрее выйти из него, следуйте простому правилу: "Исправлять за один раз только одну ошибку". Тогда вы будете знать, из-за чего возникла новая ошибка, и сможете быстро устранить ее.

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

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

Средства отладки

Трудоемкость процесса отладки всегда вызывала стремление автоматизиро­вать его. С появлением первых компиляторов стали появляться и программы- отладчики (debuggers), называемые на жаргоне программистов "дебаггерами". Они предоставляют программные средства для выполнения основных работ по отладке. С их помощью легко установить контрольные точки, сделать трассировку и пошаговое выполнение программы, просмотреть текущие зна­чения всех или выбранных переменных.

Отладчики тесно связаны с компиляторами. Для улучшения отладки компи­лятор может вставлять в машинный код дополнительную, отладочную, ин­формацию, которую отладчик использует при прогоне программы. Поэтому отладчики чаще всего поставляются вместе с компиляторами, в одной ин­тегрированной среде разработки IDE (Integrated Development Environment). В меню Options, или каком-нибудь другом меню, предназначенном для на­стройки параметров компилятора в такой интегрированной среде, можно выбрать один из нескольких режимов работы компилятора, в том числе от­ладочный режим, Debug, или окончательный режим, Release.

Во время разработки программы надо выбрать отладочный режим компиля­тора. Тогда можно будет использовать все средства отладчика, которые обычно перечислены в меню Debug интегрированной среды. После оконча­ния отладки, когда уже решено передавать программу в эксплуатацию, ком­пилятор надо перестроить на создание окончательной версии (release, build). В режиме Release компилятор удаляет из машинного кода отладочную ин­формацию и генерирует оптимизированный рабочий код.

Упражнения

1. Приведите примеры ошибок, которые выявляются не сразу, а после вы­полнения нескольких следующих операторов.

2. Приведите примеры ошибок, которые не может обнаружить компилятор.

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

4. Выясните, каково наибольшее и наименьшее целое число на вашем компьютере. Что происходит при выходе за эти значения?

5. Найдите наибольшее по абсолютной величине действительное число, которое ваш компьютер воспринимает как ноль ("машинный ноль") и ответьте на вопрос, почему сравнение с нулем действительных чисел, вроде if (х == 0.0), часто приводит к ошибке.

6. Проверьте, при всех ли значениях х выполняется тождество cos2(jc) + sin2 (jc) = 1 на вашем компьютере.

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

5[ = 1 + —+ — +...+—-—н

2 3 999999 1000000

и

1 1 1,

S2 =- +-------- +... + - + 1.

1000000 999999 2

Посмотрите, совпадут ли значения ij и 02-


Тестирование

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

Классическое определение тестирования, данное Гленфордом Дж. Майер- сом (Glenford J. Myers), звучит так: "Тестирование — процесс выполнения программы с намерением найти ошибки". Новейшие методы тестирования не только запускают программу с различными исходными и промежуточ­ными данными, но и проверяют ее исходный текст. Поэтому определение Майерса уже устарело и под тестированием сейчас понимаются любые дей­ствия, направленные на поиск ошибок в программе.

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

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

Unit-тестирование

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

Допустим, мы решили написать класс комплексных чисел complex. По ме­тодике unit-тестирования мы начинаем с того, что пишем пустой класс complex и вместе с ним сразу же пишем класс Testcomplex, который будет впоследствии содержать тесты.

class Complex{};

class TestComplex{

Complex z; public:

TestComplex(Complex z){ this.z = z;

 

}

void runTest(){ }

};

Весь этот код сразу компилируется, чтобы убедиться в правильности напи­санной конструкции. Затем начинаем разработку класса complex. В процес­се разработки каждый создаваемый метод класса complex записывается еще и в тестовый класс Testcomplex, например, следующим образом:

#include <iostream>

class Complex{

double re, im; public:

Complex(double a = 0.0, double b = 0.0){ re = a; im = b;

}

double mod(){

return sqrt(re * re + im * im);

}

};

class TestComplex{

Complex z; public:

TestComplex(Complex z) { this.z = z;

}

void runTest(double result){

cout «testMod(result) «endl;

}

bool testMod(double result){ return z.modO == result;

}

>;

void main(){

Complex zl(), z2(0.0, 1.0), z3(3.0, -4.0);

TestComplex tl(zl), t2(z2), t3(z3); tl.runTest(0.0); t2.runTest(1.0); t2.runTest(5.0);

}

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

Такая методика программирования получила название программирования, управляемого тестами (TDP, test-driven programming). Она вошла обязатель­ной составной частью в быстро получившую популярность среди разработ­чиков свободного программного обеспечения методику экстремального про­граммирования ХР (extreme programming). Unit-тестирование первоначально возникло в технологии Java. Не удивительно, что и наибольшее развитие получил свободно распространяемый программный продукт JUnit, автома­тизирующий unit-тестирование Java-ioiaccoB. Для разработчиков, пишущих программы на языке С++, по аналогии с этим программным продуктом, создана библиотека классов, названная CppUnit.

Сейчас в распоряжении программистов есть множество программных продук­тов, облегчающих unit-тестирование. Более того, есть продукты, генерирующие наборы тестов. Например, фирмой Parasoft Corporation создан и распространя­ется программный продукт JTest, подготавливающий тесты для JUnit.

Методики тестирования

За полувековую историю развития компьютеров предложено множество ме­тодик тестирования. Самая простая рассматривает программу как "черный ящик", внутренняя структура которого неизвестна. Испытатель проверяет работу "черного ящика", подавая на его вход определенные сигналы и на­блюдая, как программа реагирует на них. Первым эту методику предложил, пожалуй, Козьма Прутков, сказавший: "Щелкни кобылу в нос, она махнет хвостом".

По методике "черного ящика" каждый тест задает исходные данные програм­ме. Программа выполняется, после чего полученные результаты сравни­ваются с заранее известными тестовыми значениями. Такая методика пока­жет отсутствие ошибок только в том случае, когда набор тестов охватит все возможные исходные данные. Поскольку их необъятно много, всеобъемлю­щий охват исходных данных невозможен. Следовательно, теоретически нет уверенности, что такая методика выявит все ошибки. Кроме того, две ошибки могут взаимно уничтожаться на тестах и не будут выявлены по этой методике.

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

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

Пусть, например, надо протестировать программу, находящую площадь тре­угольника по длинам его сторон а, b, с. Если мы знаем, что эта програм­ма вычисляет отдельно площадь прямоугольного треугольника как полупро­изведение его катетов, и отдельно площадь остальных треугольников по формуле Герона, то нам не надо создавать огромное количество тестов. Мы можем разбить все множество исходных данных а, b, с на два класса — образующих прямоугольный треугольник и не образующих его — и про­гнать всего по одному тесту для каждого класса.

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

□ функциональное тестирование;

□ тестирование обращений к базам данных;

□ тестирование логики программы;

□ нагрузочное тестирование;

□ стрессовое тестирование;

□ тестирование интерфейса пользователя;

□ тестирование безопасности и прав доступа;

□ тестирование инсталляции программного продукта. Рассмотрим подробнее каждую из этих групп тестов.

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

Это основной вид тестирования, проверяющий соответствие программы функциональным требованиям, предъявленным к ней на самом первом эта­пе проектирования программного продукта. Функциональное тестирование призвано убедиться в правильности ввода исходных данных, обработки и вывода результатов, а также в правильности работы всех элементов управ­ления программой. Для этого проигрываются все сценарии использования программы (use case). При этом в тестах задаются и правильные и непра­вильные входные данные. Это позволяет проверить реакцию программы на ошибки ввода.

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

Пусть, например, надо протестировать процедуру, решающую квадратное

уравнение ax +Ьх + с = 0 с действительными коэффициентами. Для этого надо подготовить тестовый набор коэффициентов а, b, с, учитывающих все возможные ситуации. Не зная о том, что решение уравнения зависит от знака дискриминанта, мы можем упустить один из тестов, приводящих к положительному или отрицательному дискриминанту. Зато, зная алгоритм нахождения корней, мы можем разбить все тестовые значения коэффициен­тов на те, которые дают неотрицательный дискриминант, и те, для которых дискриминант отрицателен. В каждой группе можно взять только один на­бор значений коэффициентов а, b, с. Дополнив этот набор вырожденны­ми и граничными значениями, мы получим полный набор тестов. Он пока­зан в табл. 20.1.


 

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



Поделиться:


Последнее изменение этой страницы: 2016-12-11; просмотров: 377; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.137.178.133 (0.05 с.)