Лекция одиннадцатая Презумпция виновности (редакция от 17.02.87)

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

Так что будем исходить из предположения, что АС уже написан. Что же нужно делать дальше?

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

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

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

Во-вторых, мы должны быть уверены, что АС обнаружит ошибки, допущенные пользователем, а значит, мы должны проверить его не только на правильных данных, но и на неправильных.

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

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

Вернемся к тестированию. Вопрос на засыпку: с какой целью мы тестируем продукт?

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

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

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

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

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

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

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

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

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

Подведем итоги обсуждения.

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

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

Теперь нам нужно разобраться в том, что такое ошибка, иначе непонятно, что мы должны обнаруживать. Вопрос не такой уж тривиальный. Вы пока сталкивались только с двумя типами ошибок - синтаксическими, т.е. ошибками кодирования, и с программными. Поэтому вы считаете так: если результат не получен, значит, в программе есть ошибки. Все это - самые простые виды ошибок. Учтем, что поведение даже столь простого продукта, как АС, может быть довольно разнообразным. В техническом проекте мы описали самые различные случаи - отсутствие дубля, повторяемые строки и т.д. Мы должны проверить все эти ситуации и получить правильную реакцию системы.

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

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

Можно повернуть дело так: программный продукт содержит ошибку, если его поведение не соответствует спецификациям при использовании в установленных при разработке пределах. Например, при разработке АС мы допустили три типа входных данных: последовательный файл (или раздел библиотеки) по 80 байт, раздел библиотеки по 80 байт с нумерацией и раздел библиотеки по 88 байт. И на всех этих данных АС работает прекрасно. Но вот ему подают на вход, например, раздел библиотеки ДИСП-а. И АС начинает печатать чепуху. Ошибка ли это? С одной стороны вроде бы нет - ведь мы не закладывали в АС возможность работы с такими файлами. Однако с точки зрения пользователя это - ошибка. Если система не может работать с такими данными, она должна об этом сообщить. Еще пример. Система управлением движения самолетов в некотором районе рассчитана на 200 самолетов. Неожиданно появляется 201-й. Если система 'потеряет' какой-либо самолет либо вообще 'выпадет', то это, конечно же, ошибка.

Тогда, может быть, так: в программе есть ошибка, если ее поведение не соответствует документации. Но, во-первых, программы довольно часто бывают написаны лучше, чем документированы. И ошибок в документации бывает не меньше, а зачастую больше, чем в программах. Во-вторых, в документации обычно описывается 'штатная' работа. Например, в документации написано: 'нажмите кнопку ВНИМАНИЕ и введите команду'. Пользователь по запарке нажимает кнопку два раза, и система выходит из строя. Ошибка это или нет?

Разумеется, ошибка.

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

Это определение вбирает в себя лучшие свойства предыдущих определений.

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

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

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

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

Когда мы разработали алгоритмы и написали модули, не мешает 'прокрутить' всю систему на небольшом примере, чтобы убедиться в том, что алгоритмы работают правильно, что опять-таки побочные эффекты не возникают.

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

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

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

Резюмируем:

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

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

3. Тестирование собственно программы следует проводить 'сверху вниз', отыскивая сначала крупные, глобальные ошибки и лишь затем переходить к 'ловле блох'.

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

В целом они делятся на две категории:

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

(проектной и/или 'выходной').

2. Тестирование программы как белого ящика. Зная логику программы, мы подбираем тесты таким образом, чтобы проверить эту логику.

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

Когда мы тестируем 'по-белому', минимальное условие должно быть таким: каждый оператор программы должен выполняться хотя бы один раз. Это слабое условие, но необходимое. Кроме того, в каждом месте развилки (типа if thеn еlsе) нужно хотя бы раз пройти по пути 'да' и по пути 'нет'.

И в 'белом', и в 'черном' тестировании мы обязательно должны проверить граничные ситуации. На границах совершена, можно сказать, половина ошибок. если у нас в эталоне может быть от одной до 10000 строк, обязательно нужно пропустить тест для 1 строки и для 10000 строк, для 0 строк и 1001 строки, ну и, конечно, для какого-нибудь случая внутри этого интервала.

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

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

Например, для параметра рстр=хх (хх - размер страницы) мы должны опробовать как минимум такие тесты:

1. Проверка указания параметра

( рст, ртр, рстр и т.д.)

2. Отсутствие = (рстр 60)

3. Укороченное число

рстр=1

4. Неверное число

рстр=хо

И т.д., не говоря уже о их комбинациях (например, рстр=х0). В книге Майерса 'Искусство тестирования программ' вы найдете более подробное описание тестирования. Мы же, в силу нехватки времени, не можем углубляться в эту тему. Впрочем, мы еще поговорим о тестировании, когда будем обсуждать проблему групповой разработки программ.

На оглавление


© Алексей Бабий 1980-1986