VIII.2.3. М е т о д к о н т р о л ь н ы х т е с т о в Никогда не берите на корабль два хронометра, берите один или, если есть возможность, три, но не два. Наставление мореплавателям начала ХIХ века ...путем тестирования никогда нельзя установить от- сутствие ошибок в программе. Дело в том, что невоз- можно определенно утверждать, что при проверке най- дена последняя ошибка в программе; следовательно,ни- когда нельзя быть уверенным,что может быть найдена и первая ошибка. Р.Лингер, Х.Миллс, Б.Уитт Как бы ни была тщательно проверена и "прокручена" программа за столом, решающим этапом, устанавливающим ее пригодность для работы, является конт- роль программы по результатам ее выполнения на компьютере. Мы рассмотрим здесь универсальный метод контроля - м е т о д к о н - т р о л ь н ы х т е с т о в ("test" - "испытание", "проверка"). Т е с т и р о в а н и е - это процесс исполнения программы на компью- тере с целью о б н а р у ж е н и я ошибок [50]. Поясним это определение. Т е с т о м будем называть информацию, состоящую из исходных данных, специально подобранных для отлаживаемой программы, и из соответствующих им эталонных результатов (не только окончательных, но и промежуточных), используемых в дальнейшем для контроля правильности работы программы. Если поставить целью демонстрацию о т с у т с т в и я ошибок, то мы подсознательно будем стремиться к этой цели, выбирая тестовые данные, на которых вероятность появления ошибки мала. В то же время, если нашей зада- чей станет обнаружение ошибок, то создаваемый нами тест будет обладать бо- льшей вероятностью обнаружения ошибки. Такой подход заметнее повысит каче- ство программы. Тестирование - процесс деструктивный (т.е.обратный созидательному, кон- структивному). Именно этим и объясняется, почему многие считают его труд- ным. Большинство людей склонно к конструктивному процессу созидания объек- тов и в меньшей степени - к деструктивному процессу разделения на части. Для усиления определения тестирования проанализируем два понятия - "удач- ный" и "неудачный". Большинство назовет тестовый прогон неудачным, если обнаружена ошибка и,наоборот, удачным, если он прошел без ошибок. Из опре- деления тестирования следует противоположное: ┌──────────────────────────────────────────────────────────────────────┐ │ тестовый "прогон" будем называть у д а ч н ы м, если в процессе │ │ его выполнения обнаружена ошибка, и н е у д а ч н ы м, если по- │ │ лучен корректный результат │. └──────────────────────────────────────────────────────────────────────┘ Вопрос о позиции программиста по отношению к продукту его труда связан, как это показано Вейнбергом [68], с принципами безличного программирова- ния и когнитивного диссонанса. К о г н и т и в н ы й д и с с о н а н с - это психологический принцип, который руководит действиями человека, чьи представления о себе оказались под угрозой. "Программист, который искренне считает программу продолжени- ем своего "я", не будет пытаться найти все ошибки в ней. Напротив, он по- старается показать, что программа правильна, даже если это означает не за- мечать ошибок,чудовищных для постороннего взгляда...Человеческий глаз име- ет почти безграничную способность не видеть то, чего он видеть не желает" [68]. Спасти в такой ситуации может безличное программирование. Вместо того, чтобы быть скрытным и защищать свою программу, программист занимает проти- воположную позицию: он открыто приглашает других программистов читать и конструктивно критиковать ее. Когда кто-то находит ошибку в его программе, программист,конечно, не должен радоваться, что ошибся; его позиция пример- но такова: "О! Мы нашли ошибку в н а ш е й программе! Хорошо, что мы нашли ее сейчас, а не позже! Поучимся на этой ошибке, а заодно посмотрим, не найдем ли еще!" Программист, обнаруживший ошибку в чужой программе, не кричит: "Посмотри на свою идиотскую ошибку!", а реагирует примерно так: "К а к любопытно! Интересно, не сделал ли и я такой ошибки в написанном мною модуле?" При использовании метода тестов ┌────────────────────────────────────────────────────────────────────┐ │ программа (или отдельный ее блок) считается п р а в и л ь н о й, │ │ если пропуск программы для выбранной системы тестовых исходных │ │ данных дает правильные результаты │ . └────────────────────────────────────────────────────────────────────┘ Таким образом, контроль программы сводится к тому, чтобы подобрать сис- тему тестов, получение правильных результатов для которой гарантировало бы правильную работу программы и для остальных исходных данных из области, указанной в решаемой задаче. Для реализации метода контрольных тестов должны быть изготовлены или заранее известны э т а л о н н ы е результаты, на основании сверки с ко- торыми получаемых тестовых результатов, можно было бы сделать вывод о пра- вильности работы программы на данном тесте. Э т а л о н н ы е результаты для вычислительных задач можно получить, осуществляя вычисления вручную, применяя результаты, полученные ранее на другом компьютере или по другой программе, или, используя известные факты, свойства, физические законы. Разрабатывая систему тестов, нужно стремиться к тому, чтобы успешный пропуск ее на компьютере д о к а з ы в а л наличие ошибок в программе (или отдельном ее блоке), хотя для многих достаточно сложных программ,осо- бенно если над ними работает несколько программистов, можно практически говорить лишь о большей или меньшей вероятности правильности программы. Это объясняется тем, что изготовление и пропуск в с е х тестов, необхо- димых для доказательства, может потребовать такого объема работ, который затянет этап контроля на многие месяцы или годы. Поэтому при разработке системы тестов наряду с задачей всестороннего и глубокого тестирования, стоит задача минимизации количества необходимых тестовых результатов, ма- шинного времени и усилий программиста. В большинстве случаев при использовании метода контрольных тестов во- прос о д о к а з а т е л ь с т в е о т с у т с т в и я ошибок практи- чески можно ставить лишь для небольших блоков (модулей) программы, а для целой программы приходится ограничиваться той или иной вероятностью отсут- ствия ошибок в программе. Неоднократно экспериментально установлено, что в любой сложной програм- ме в процессе эксплуатации обнаруживаются ошибки, даже если проведено са- мое тщательное тестирование. Тем самым утверждается объективная реаль- ность, заключающаяся в невозможности формализовать и обеспечить абсолют- ную полноту всех эталонных значений, а также провести всеобъемлющее исчер- пывающее тестирование и устранить в с е ошибки в сложных программах. Опыт показывает, что до начала тестирования число ошибок в сложных про- граммах - порядка 1-2% от общего числа операторов в программе. Самое тща- тельное тестирование сложных программ позволяет получить программы с веро- ятностью ошибки в каждом операторе 0.0001 ÷ 0.00001,т.е. несколько ошибок может остаться. ┌──────────────────────────────────────────────────────────────────┐ │ После завершения тестирования программы в течение нескольких │ │ лет эксплуатации могут быть выявлены еще десятки ошибок! │ └──────────────────────────────────────────────────────────────────┘ VIII.2.3.1. С т р а т е г и я т е с т и р о в а н и я Вы должны радоваться, что мост разрушился,- я планировал построить еще тринадцать по тому же проекту. Замечание, приписываемое Х.К.Брюнелю, адресованное дирекции Большой запад- ной железной дороги Перечислим основные принципы тестирования [50]. Эти принципы интересны тем, что в основном они интуитивно ясны, но в то же время на них часто не обращают должного внимания. 1. Описание предполагаемых значений выходных данных или результатов должно быть необходимой частью тестового набора. Нарушение этого очевидного принципа представляет одну из наиболее рас- пространенных ошибок. Ошибочные, но правдоподобные результаты могут быть признаны правильными, если результаты теста не были заранее определены. Здесь мы сталкиваемся с явлением психологии: мы видим то,что мы хотим уви- деть. Другими словами,несмотря на то,что тестирование по определению - де- структивный процесс,есть подсознательное желание видеть корректный резуль- тат. Один из способов борьбы с этим состоит в поощрении детального анали- за выходных переменных заранее, еще при разработке теста. 2. Следует избегать тестирования программы ее а в т о р о м . Многие, кому приходилось самому делать дома ремонт, знают, что процесс обрывания старых обоев (деструктивный процесс) не легок, но он просто не- выносим, если не кто-то другой, а Вы сами первоначально их наклеивали.Вот так же и большинство программистов не может эффективно тестировать свои программы, потому что им трудно демонстрировать собственные ошибки. 3. Необходимо досконально изучать результаты применения каждого теста. Представляется достоверным, что значительная часть всех обнаруженных в конечном итоге ошибок могла быть выявлена в результате самых первых тес- товых прогонов, но они были пропущены вследствие недостаточно тщательного анализа результатов первого тестового прогона. 4. Тесты для неправильных и непредусмотренных входных данных следует разрабатывать так же тщательно, как для правильных и предусмотренных. Вполне вероятно,что тесты, представляющие неверные и неправильные вход- ные данные, обладают большей обнаруживающей способностью, чем тесты, соот- ветствующие корректным входным данным. 5. Необходимо проверять не только, делает ли программа то,для чего она предназначена, но и не делает ли она того, что не должна делать. Обязательно проверяйте программу на нежелательные побочные эффекты. 6. Не следует выбрасывать тесты, даже если программа уже не нужна. Необходимость в использованных тестах наиболее часто возникает в интерак- тивных системах отладки. ┌───────────────────────────────────────────────────────────────┐ │ Обычно тестирующий сидит за терминалом, на лету придумывает │ │ тесты и запускает программу на выполнение. │ └───────────────────────────────────────────────────────────────┘ При такой практике работы после применения тесты пропадают. После вне- сения изменений или исправления ошибок необходимо повторить тестирование, тогда приходится заново изобретать тесты. Как правило, этого стараются из- бегать, поскольку повторное создание тестов требует значительной работы. 7. Нельзя планировать тестирование в предположении, что ошибки не бу- дут обнаружены. ┌────────────────────────────────────────────────────────────────┐ 8. │ Вероятность наличия необнаруженных ошибок в части программы │ │ пропорциональна числу ошибок, уже обнаруженных в этой части. │ └────────────────────────────────────────────────────────────────┘ На первый взгляд, этот принцип лишен смысла, но тем не менее подтверж- дается многими программами. Например, допустим,что некоторая программа со- стоит из модулей А и В. К определенному сроку в модуле А обнаружено пять ошибок, а в модуле В - только одна, причем модуль А не подвергался более тщательному тестированию. Тогда из рассматриваемого принципа следует, что вероятность необнаруженных ошибок в модуле А больше, чем в модуле В. Спра- ведливость этого принципа подтверждается еще и тем, что для ошибок свойст- венно располагаться в программе в виде неких скоплений, хотя данное явле- ние пока никем еще не объяснено. Таким образом, если в какой-нибудь части программы обнаружено больше ошибок, чем в других, то на ее тестирование должны быть направлены допол- нительные усилия. 9. Тестирование - процесс творческий. Вполне вероятно, что для тестиро- вания большой программы требуется больший творческий потенциал,чем для ее проектирования. Чтобы подчеркнуть некоторые мысли, высказанные в этом разделе,приведем еще раз три наиболее важных принципа тестирования. ┌───────────────────────────────────────────────────────────────────┐ │ Т е с т и р о в а н и е - это процесс выполнения программ на │ │ компьютере с целью о б н а р у ж е н и я ошибок. │ │ Х о р о ш и м считается тест, который имеет высокую вероятность │ │ обнаружения еще не выявленной ошибки. │ │ У д а ч н ы м является тест, который обнаруживает еще не │ │ выявленную ошибку. │ └───────────────────────────────────────────────────────────────────┘ "На закуску" рекомендуем выполнить следующий простой тест.Задача состо- ит в том, чтобы проверить программу,которая по трем заданным числам печа- тает сообщение о том, является ли треугольник со сторонами, длины которых равны данным значениям, неравносторонним, равнобедренным или равносторон- ним [51]. Напишите на листе бумаги набор тестов, которые, как Вам кажется, будут адекватно проверять эту программу. Построив свои тесты,проанализируйте их. Приступайте к работе... Следующий шаг состоит в оценке эффективности Вашей проверки. Оказывает- ся, что программу труднее написать, чем это могло показаться вначале.Были изучены различные версии данной программы и составлен список общих ошибок. Оцените Ваш набор тестов, попытавшись с его помощью ответить на приведен- ные ниже вопросы. За каждый ответ "да" присуждается одно очко. 1. Составили ли Вы тест, который представляет правильный неравносторон- ний треугольник? (Заметим, что ответ "да" на тесты,со значениями 1,2,3, и 2,5,10 не обоснован, т.к. не существует треугольников, имеющих такие сто- роны.) 2. Составили ли Вы тест,который представляет правильный равносторонний треугольник? 3. Составили ли Вы тест,который представляет правильный равнобедренный треугольник? (Тесты со значениями 2,2,4 принимать в расчет не следует.) 4. Составили ли Вы,по крайней мере,три теста, которые представляют пра- вильные равнобедренные треугольники, полученные как перестановки двух рав- ных сторон треугольника (например, 3,3,4; 3,4,3, и 4,3,3)? 5. Составили ли Вы тест, в котором длина одной из сторон треугольника принимает нулевое значение? 6. Составили ли Вы тест, в котором длина одной из сторон треугольника принимает отрицательное значение? 7. Составили ли Вы тест, включающий три положительных целых числа, сум- ма двух из которых равна третьему? (Другими словами,если программа выдала сообщение о том, что числа 1,2,3 представляют собой стороны неравносторон- него треугольника, то такая программа содержит ошибку.) 8. Составили ли Вы, по крайней мере, три теста с заданными значениями всех трех перестановок, в которых длина одной стороны равна сумме длин двух других сторон (например, 1,2,3; 1,3,2 и 3,1,2)? 9. Составили ли Вы тест из трех целых положительных чисел, таких, что сумма двух из них меньше третьего числа (т.е. 1,2,4 или 12,15,30)? 10. Составили ли Вы, по крайней мере, три теста из категории 9, в кото- рых Вами испытаны все три перестановки (например: 1,2,4; 1,4,2 и 4,1,2)? 11. Составили ли Вы тест, в котором все стороны треугольника имеют дли- ну, равную нулю (т.е. 0,0,0)? 12. Составили ли Вы по крайней мере один тест,содержащий нецелые значе- ния? 13. Составили ли Вы хотя бы один тест, содержащий неправильное число значений (например, два, а не три целых числа)? Конечно, нет гарантий, что с помощью набора тестов, который удовлетво- ряет вышеперечисленным условиям, будут найдены все возможные ошибки.Но по- скольку вопросы 1÷13 представляют ошибки, имевшие место в различных верси- ях данной программы, адекватный тест для нее должен их обнаруживать. Отметим, что опытные профессиональные программисты набирают в среднем только 7÷8 очков из 14 возможных. Выполненное упражнение показывает, что тестирование даже тривиальных программ, подобных приведенной, - не простая задача. VIII.2.3.2. Т а к т и к а т е с т и р о в а н и я Перевести программу из хорошего состояния в отличное неизмеримо труднее, чем из плохого в удовлетворительное. Программистский фольклор Поговорим о методах тестирования. При н е у п о р я д о ч е н н о м тестировании ("smoke test" - "гру- бая проверка работоспособности простым запуском", "дымовой тест") исход- ные данные, имитирующие внешнюю среду, случайным образом генерируются во всем диапазоне возможного изменения параметров. При этом многие значения исходных данных характеризуются малой вероятностью обнаружения ошибок и не оправдывают затраты на выполнение тестирования. Кроме того,возможно по- явление логически противоречивых данных. В то же время данные, наиболее важные с позиции реального использования программ и возможностей обнаруже- ния ошибок, могут оказаться неохваченными в процессе тестирования. Поэтому на практике последовательно применяют следующие методы тестиро- вания: с т а т и ч е с к и й, д е т е р м и н и р о в а н н ы й и с т о- х а с т и ч е с к и й. С т а т и ч е с к о е тестирование ("static check") является наиболее формализованным методом проверки корректности программ. Тестирование про- водится без исполнения программы путем формального анализа текста програм- мы на языке программирования. Операторы и операнды текста программ при этом анализируются в символьном виде, поэтому такой метод называют также с и м в о л и ч е с к и м тестированием. Наиболее трудоемкими и детализирующими являются методы д е т е р - м и н и р о в а н н о г о тестирования. При детерминированном тестирова- нии контролируется каждая комбинация исходных эталонных данных и соответ- ствующая ей комбинация эталонных результатов. Разумеется, в сложных про- граммах невозможно перебрать все комбинации исходных данных и проконтро- лировать результаты функционирования программы на каждой из них. В таких случаях применяется с т о х а с т и ч е с к о е тестирование, при кото- ром исходные тестовые данные задаются множеством случайных величин с со- ответствующими распределениями и для сравнения полученных результатов ис- пользуются также распределения случайных величин. В результате при стохас- тическом тестировании возможно более широкое варьирование исходных данных, хотя отдельные ошибки могут быть не обнаружены, если они мало искажают средние статистические значения или распределения. ┌──────────────────────────────────────────────────────────────────────┐ │ Стохастическое тестирование применяется в основном для обнаружения │ │ ошибок, а для диагностики и локализации ошибок приходится пере- │ │ ходить к детерминированному тестированию с использованием конкрет- │ │ ных значений параметров из области изменения использовавшихся слу- │ │ чайных величин. │ └──────────────────────────────────────────────────────────────────────┘ Рассмотрим некоторые п р а в и л а тестирования, в которых делается попытка учесть как желательность доказательства правильности контролируе- мой программы, так и ограниченность человеческих возможностей при проведе- нии такого доказательства [49]. П р о х о д у ч а с т к о в. Каждый линейный участок программы должен быть обязательно пройден при выполнении по крайней мере, одного теста. Очевидно, что в противном случае никакой гарантии в правильности работы всей программы дать будет нельзя. В том случае, когда выполнение некоторого участка программы меняет по- рядок выполнения или характер работы других участков, может потребоваться перебор всех ветвей программы,т.е. проход по всем возможным путям выполне- ния программы (многократная проверка требуется, в частности, для участков, содержащих переменные с индексами). Т о ч н о с т ь п р о в е р к и. Контроль арифметических блоков (как и других блоков) производится путем сверки результатов, полученных при вы- полнении блока, с эталонными результатами. Для арифметических результатов дополнительная сложность заключается в определении точности, с которой не- обходимо сверять (и, тем самым, вычислять) эталонные и тестовые результа- ты, с тем, чтобы можно было действительно удостовериться в правильности работы блока. Дело в том, что величины, входящие в проверяемое арифметическое выраже- ние, в зависимости от соотношения их значений и характера производимых над ними операций, вносят различный вклад в результат. Поэтому может ока- заться, что неправильно запрограммированное выражение для некоторых тесто- вых значений величин, входящих в него, будет иметь я к о б ы правильное значение ввиду того, что результат неправильной операции или неверно вы- численный ранее операнд выражения не окажут почти никакого влияния на тес- товое (сравниваемое) значение выражения. Например, для оператора C=A+B из того, что значение C совпало с эталон- ным значением, не следует, что выражение записано в программе верно, по- скольку для случая, когда A>>B, замена плюса на минус не будет обнаружена, если эталонное значение C вычислено с недостаточной точностью. Кроме того, если вычисление А и B не было проверено ранее, то из правильности C нель- зя сделать вывод о правильности вычисления B (для случая A>>B). Таким образом, для того, чтобы быть уверенным в том, что правильный числовой результат, полученный на компьютере, говорит о правильности про- граммы, необходимо следить за промежуточными результатами вычислений, ко- торые не должны выходить за определенный диапазон, устанавливаемый в зави- симости от точности вычислений эталонных результатов. Выполнение такого требования может привести к необходимости многократной проверки выражения для различных диапазонов данных. М и н и м а л ь н о с т ь в ы ч и с л е н и й. Когда продолжительность работы контролируемой программы и, тем самым, количество вычислений и необходимых для контроля тестовых данных зависит от каких-либо параметров, то при контроле их следует выбирать такими, что- бы они минимизировали количество вычислений. К таким параметрам, например, могут относиться : α) шаг или отрезок интегрирования; β) порядок матрицы или количество элементов вектора; γ) длина символьных строк; σ) точность для итерационных вычислений и т.п. . Конечно, такая инициализация не должна значительно снижать надежность контроля. Следует заметить также, что значения исходных данных нужно вы- бирать такими, чтобы изготовление эталонных результатов вручную было, по возможности, облегчено. Например, данные могут быть сначала взяты целочис- ленными или такими, чтобы при проверке выражений некоторые их слагаемые, уже проверенные ранее, обращались в нуль. Д о с т о в е р н о с т ь э т а л о н о в. Нужно обратить внимание и на достоверность процесса получения эталонных результатов. По возможности, они должны вычисляться не самим программистом, а кем-то другим, с тем,что- бы одни и те же ошибки в понимании задания не проникли и в программу, и в эталонные результаты. Если тесты готовит сам программист, то эталоны нуж- но вычислять до получения на компьютере соответствующих результатов.В про- тивном случае имеется опасность невольной подгонки вычисляемых значений под желаемые, полученные ранее на компьютере. В качестве эталонных резуль- татов часто используют и данные,полученные при ручной прокрутке программы.