Говорят, что попытка запуска одного из первых искусственных спутников Земли закончилась неудачей, так как в одной из программ была неверно поставлена десятичная точка. Ошибки программирования редко приводят к столь серьезным последствиям, однако известно, что отыскание ошибок в логике программ стоит огромного количества времени и сил. Средства, затрачиваемые на это, исчисляются миллионами долларов. Ошибки же в программах операционной системы приводят к значительным простоям дорогостоящего оборудования. Телефонная компания, использующая автоматизированную систему расчетов, может выплатить абоненту 7000 долларов, вместо того чтобы получить с него 7. А кому не приходилось встречаться с определенными недоразумениями, связанными с использованием ЭВМ для расчетов с помощью кредитных карточек?
Предыдущий раздел был посвящен описанию процедуры, используемой в тех случаях, когда обнаружение ошибки аппаратурой или операционной системой приводит к выдаче дампа. После того как все ошибки, приводящие к прекращению выполнения программы, устранены, появляется желание сказать: «Дампа нет, следовательно, программа работает правильно». Желание тем сильнее, чем скорее требуется закончить работу. Однако этого не следует говорить до тех пор, пока не будет проведено достаточно полное тестирование.
Модульное программирование
В большинстве случаев будет, по меньшей мере, неосторожно заключить, что программа не содержит ошибок, если она правильно выполняется и приводит к получению искомых результатов для одного тестового набора исходных данных. Более того, многие решаемые задачи настолько сложны, что тестирование путем задания некоторых стандартных исходных данных не дает никакой уверенности в отсутствии ошибок. Таким образом, наша задача сводится к определению, содержит ли данная программа или набор программ ошибки или нет. Если ошибки содержатся в самой логике программ (мы, естественно, сейчас исключаем ошибки, обнаруживаемые аппаратными средствами или операционной системой), то как организовать их поиск в достаточно сложных случаях?
Конечно, уже при составлении программы следует предусмотреть возможность возникновения описанных трудностей. Для этого обычно крупные программы подразделяются на более мелкие, так называемые модули, каждый из которых предназначен для решения узкой и специфичной задачи.
Идеальным, вообще говоря, является случай, когда таким образом составленные модули независимы в том смысле, что изменение, внесенное в один из них, никак не влияет на работу остальных. Это означает, что для устранения ошибки, содержащейся в одном из модулей, достаточно внести исправления только лишь в сам модуль. Полная независимость модулей практически почти никогда не достижима, тем не менее, некоторое приближение к идеальному случаю достигается при подготовке значительных по размеру программ в форме пакетов. Такие программы значительно удобнее как для составления, так и для откладки, чем те, которые пишутся без использования модульного принципа.
Во многих реальных случаях решаемая задача настолько сложна, что требует участия в работе довольно большого числа программистов. Обычно небольшая группа программистов выполняет составление, отладку и тестирование одного модуля. Каждой такой группе должно быть точно известно, какие требования предъявляются к составляемому ими модулю, какие исходные данные он должен обрабатывать, какие результаты и в каком формате должен получать. Подобный групповой способ программирования требует тщательной подготовки, планирования и управления всей работой, так чтобы каждая группа точно знала, что требуется непосредственно от нее.
Рис. 12.6. Блок-схема программы учета запасов.
Деление программы на модули в большинстве случаев очевидно, поскольку решаемая задача, как правило, состоит из нескольких основных частей. Обычно модули, предназначенные для выполнения некоторых логически связанных между собой функций, объединяются в так называемые подпрограммы. Каждая подпрограмма на самом деле представляет собой независимую программу и содержит, по крайней мере, один модуль.
Посмотрим теперь на рис. 12.6. На нем изображена общая блок- схема некоторой программы организации учета запасов, базирующейся на использовании памяти на магнитных дисках. В данном случае управляющей программой используются четыре подпрограммы: подпрограмма ввода с карт, подпрограмма печати, подпрограмма корректировки записей и, наконец, подпрограмма, организующая работу с памятью на магнитных дисках. В приведенной ниже таблице указаны подпрограммы и содержащиеся в них модули.
Подпрограмма |
Модули |
Ввод с карт |
1. Ввод с карт 2. Преобразование данных
|
Печать |
1. Преобразование данных 2. Вывод на печать
|
Корректировка записей |
1. Изменение общего количества 2. Изменение учетного номера 3. Изменение цены 4. Другие
|
Работа с дисками |
1. Считывание текущей информации в память 2. Обновление информации 3. Запись на диск новых данных
|
Центральная часть блок-схемы представляет собой управляющую
программу, функции которой состоят в осуществлении контроля за порядком выполнения подпрограмм и задании им исходных данных. Таким образом, задача основной управляющей программы состоит в интерпретации входных команд и реакции на них посредством запросов на выполнение соответствующих подпрограмм, вызовов подпрограмм (подробнее о подпрограммах см. в гл. 13).
Типичной операцией учета является изменение в ведомости общего количества товаров данного типа в связи с продажей некоторых из них. Для внесения соответствующего изменения в запись ведомости должны быть выполнены следующие действия:
1. Вызов управляющей программой подпрограммы ввода с перфокарт для считывания информации с управляющей карты. В состав этой информации входят учетный номер, код требуемого изменения и его количественное значение.
2. Считывание карты модулем ввода.
3. Проверка введенных данных модулем преобразования и преобразование числовой информации в форму, подходящую для последующего использования.
4. Проверка управляющей программой введенной управляющей информации, определение требуемой операции и вызов подпрограммы работы с дисками.
5. Считывание информации с диска модулем ввода подпрограммы работы с дисками.
6. Вызов подпрограммы корректировки.
7. Работа модуля изменения общего количества подпрограммы обработки записей.
8. Вызов управляющей программой подпрограммы обмена с дисками.
9. Возврат модифицированной информации на соответствующее место дисковой памяти модулем записи на диск новых данных вызванной подпрограммы.
10. Вызов подпрограммы печати, производящей преобразование формы представления данных и вывод этих данных на печать.
Заметим, что в рассмотренном примере каждый модуль выполняет свою специфическую функцию. Если бы программа учета была составлена как единое целое, т. е. без подразделения на модули и подпрограммы, то задача ее отладки стала бы значительно более сложной, если не невыполнимой. После устранения ошибок, обнаруженных ЭВМ или операционной системой, программа может получить некоторые результаты, но ведь они могут быть и неверны:
Предположим, например, что в результате выполнения учетной операции было выведено на печать отличающееся от истинного значение общего количества каких-то товаров. Может быть, управляющая карта содержит неправильные данные? А может быть, ошибка допущена при преобразовании формы представления информации? Или что-то не так с самой корректировкой?
Модульное программирование позволяет избежать встречи с подобной ситуацией. Каждый модуль пишется и, отлаживается отдельно. Затем модули объединяются в подпрограммы, и снова каждая подпрограмма проходит свой набор тестов. Только после полной отладки все модули и подпрограммы собираются вместе, и производится попытка организации их работы под управлением основной программы. Если предположить, что все подпрограммы работают правильно, то в случае общей неверной работы ошибки следует искать либо в управляющей программе, либо в организации обменов данными между подпрограммами.
Прежде чем перейти к подробному рассмотрению процессов отладки и тестирования модулей, укажем на еще одно преимущество модульного программирования. Разбиением программы на более мелкие части достигается значительное увеличение ее гибкости, т. е. возможности ее модификации. Предположим, что мы решили для ввода и вывода управляющей информации и выдачи результатов использовать терминал вместо устройства ввода с перфокарт и печати. Если модули, выполняющие преобразование данных, составлены как следует, то нам всего лишь требуется заменить модули считывания с карт и печати на модули ввода-вывода на терминал. Если мы хотим хранить" учетную информацию не на дисках, а на лентах, то модуль обмена с дисками следует заменить на модуль обмена с лентой. Для увеличения множества операций корректировки текущей информации в программу следует добавить новые модули, а также внести некоторые новые команды в подпрограмму модификации для обеспечения возможности работы этих вновь созданных введенных модулей.
Подобная гибкость требует практической независимости отдельных модулей, поэтому отладка и тестирование этих модулей должны также производиться независимо. Перейдем теперь к непосредственному рассмотрению вопросов тестирования и отладки модулей.
Тестирование и отладка модулей
Независимо от того, является ли данный модуль частью вновь создаваемой или должен быть введен в уже существующую систему, к отладке его следует приступать еще до начала перфорации. Попытайтесь определить, к каким результатам может привести задание вашей программе различных наборов исходных данных, учитывая, что, вообще говоря, не всегда исходная информация будет задаваться правильно. Дополните модуль командами, проверяющими корректность входных данных. Выполнение таких проверок полезно не только на этапе отладки, но и в процессе работы модуля как части некоторой общей системы. Внимательно просмотрите все циклы. Что в действительности будет происходить, если количество прохождений некоторого цикла должно быть равно 0? Существует ли ограничение на число прохождений цикла? Тщательно проверьте организацию всех передач управления. Такой предварительный просмотр программы несколько утомителен и может показаться малоэффективным, однако в большинстве случаев он позволяет впоследствии избежать довольно значительных дополнительных затрат.
Все примеры и упражнения данной главы достаточно просты для того, чтобы соответствующие программы могли состоять всего из одного модуля, включающего средства выполнения операций ввода и вывода. В более сложных случаях ввод и вывод обеспечивается далеко не в каждом модуле и далеко не каждый модуль представляет собой законченную программу. Примером может служить, например, модуль изменения общего количества товаров, изображенный на рис. 12.6.
В подобных ситуациях отладка и тестирование требуют дополнительного программирования.
Нашей целью является по возможности более точное моделирование условий работы каждого модуля. Если модуль не представляет собой отдельную законченную подпрограмму, то к нему следует временно добавить предложения, обеспечивающие связь подпрограмм, для создания возможности его использования в операционной системе. Разумеется, это совместное использование следует организовать так, чтобы оно не оказывало существенного влияния на процессы ввода, вывода и порядок вычислений в самом модуле. В противном случае после устранения временно внесенных предложений и включения модуля в состав некоторой крупной программы система в целом может оказаться неработоспособной.
Если ввод необходимой для работы модуля информации выполняется вне самого модуля, то необходимо составить так называемую управляющую программу, или генератор входных данных, которая обеспечивает передачу модулю входных данных в соответствующей форме. Например, модуль изменения общего количества товаров должен получать последовательность записей с диска, а кроме того, количественные значения изменений, которые следует произвести. В процессе отладки эти данные должны задаваться генератором входных данных.
Составление управляющей программы и выбор значений, которые должны получаться на ее выходе, вообще говоря, является нетривиальной задачей. Мы хотим проверить модуль достаточно полно. Генератор должен формировать совокупность входных данных, с помощью которых можно проверить все ветви модуля. Однако недостаточно отладить работу модуля со входными данными, удовлетворяющими нормальным условиям его применения. Нужно также проверить работу модуля со входными данными, либо содержащими ошибки, либо выходящими за пределы допустимой области значений.
В обычном случае модуль изменения общего количества производит сложение или вычитание заданного значения из числа, находящегося в поле общего количества обрабатываемой записи. Но следует предусмотреть возможность возникновения ненормальных ситуаций. Если в результате вычислений общее количество товаров получается отрицательным, что свидетельствует о наличии ошибки во входных данных или в модифицируемой записи, а возможно, о продаже отсутствующего товара, модулем должна быть зафиксирована особая ситуация (обычно это достигается посылкой сообщения об ошибке основной программе). Модуль должен проверить, что производится модификация именно той записи, которой нужно. Для этого следует сравнить учетный номер записи с номером, задаваемым управляющей картой. Если номера не совпадают, то нужно послать предупреждающее сообщение пользователю. Возможность возникновения различных ситуаций такого рода должна быть рассмотрена на этапах проектирования и программирования модуля, его отладки, тестирования, но ни в коем случае не после начала использования модуля в качестве составной части некоторого пакета.
В программах вычислительного типа ошибочные случаи должны быть рассмотрены с такой же тщательностью, как и нормальные. Возьмем в качестве примера программу обращения матриц. Предположим, что мы обратили вручную матрицу размером 3 на 3, затем использовали для обращения написанную программу и получили тот же результат. Вам следует четко представлять, что это только начало тестирования. К сожалению, некоторые программисты, успешно пройдя лишь этот первый этап, полагают, что составленные ими модули работают должным образом. Тем не менее, осталось еще несколько невыясненных вопросов. Мы вообще привыкли мыслить категориями трехмерного пространства, поэтому вполне вероятно предположить, что создаваемые нами программы гораздо лучше подходят для работы именно в трехмерных случаях. Итак, следует проверить правильность работы программы и тогда, когда размерность матрицы больше чем 3, и тогда, когда она меньше. Программа должна охватывать и такие специальные случаи, как обращение матриц размерностью 0x0, 1x1, 2x2. Также правильно должно выполняться обращение матриц с высокой размерностью.
Что можно сказать о вырожденных (необратимых) матрицах? Модуль должен производить проверку условия вырожденности и в случае его выполнения посылать соответствующее сообщение вызывающей программе и возвращать ей управление вместо попыток деления на 0, неизбежно производящихся при отсутствии подобных проверок. Одним из тестов должно служить задание матриц, близких к вырожденным (при обращении таких матриц обычно получаются значительные ошибки). Информация о прохождении программой всех тестов, ограничениях на размерность обращаемых матриц и величине ошибок, возникающих вследствие округлений, должна быть внесена в документацию, прилагаемую к модулю.
Короче говоря, независимо от области предполагаемого применения программы должны проверяться на работу не только с нормально заданной входной информацией, но и с ошибочными или выходящими за предусмотренные пределы данными. Иногда полезно исследовать и некоторые предельные случаи.
Если модуль не обеспечивает вывода на печать, то необходимо добавить соответствующие команды для обеспечения возможности проверки правильности его выполнения. Несколько раньше мы обсуждали использование дампа в случае фиксации ошибки самой машиной или операционной системой. В языке ассемблера эффективным способом получения результатов от программы, не выдающей ничего на печать, является запрос выдачи дампа. Для этой цели предусмотрена специальная макрокоманда ABEND, которая в общем виде выглядит следующим образом:
ABEND n, DUMP
Дамп, выдаваемый по команде ABEND, ничем не отличается JOT уже рассматривавшихся нами дампов. Параметр n представляет собой пользовательский код завершения, который появляется в распечатке вместе с дампом.
Макрокоманда ABEND вызывает прекращение выполнения программы и поэтому делает невозможным продолжение отладки модуля в пределах того же задания. Во многих конкретных вычислительных системах существуют средства, позволяющие распечатывать содержимое памяти и регистров по требованию, не вызывая прекращения выполнения основной программы. Вы можете пользоваться также своими собственными отладочными макро. Существует, кроме того, процедура PDUMP, входящая в набор стандартных процедур языка ФОРТРАН. Используя PDUMP, можно распечатать содержимое указанных областей памяти в одном из нескольких форматов. Вызывается эта процедура обычными командами вызова подпрограмм. Более подробно эти вопросы освещены в руководстве «Библиотека OS ФОРТРАН IV. Вычислительные и обслуживающие подпрограммы» (GC28—6818).
Не бойтесь выдачи большого количества промежуточной информации при отладке или тестировании программ. Следует использовать все находящиеся в нашем распоряжении возможности для облегчения процесса поиска ошибок. Однако, конечно же, не мешает подумать, какая конкретно информация вам нужна. Это может сэкономить значительное количество не только вашего, но и машинного времени.
Сборка программы
После того как модули окончательно проверены и отлажены, встает вопрос о компоновке из этих модулей программного пакета.
Если несколько модулей являются составными частями какой-то подпрограммы, то эта подпрограмма должна включать в себя предложения, определяющие, какие именно модули принадлежат ей. Например, подпрограмма обработки записей, изображенная на рис. 12.6, состоит из нескольких модулей. При вызове подпрограммы должно однозначно определяться, какие именно модули следует использовать. Эта цель обычно достигается выполнением команд, анализирующих управляющий код, задаваемый основной программой в виде одного из элементов входных данных.
Каждая подпрограмма должна быть отлажена точно таким же образом, что и входящие в нее модули. Это обычно требует разработки генераторов входных данных и программ вывода. Снова необходимо проверить правильность работы каждой подпрограммы и обеспечить диагностику возможных ошибок.
Наконец мы добрались до управляющей программы. Как правило, эта программа не производит никаких вычислений, она лишь определяет порядок выполнения подпрограмм и передает им соответствующие данные и управляющую информацию. Зная, какие действия выполняются отдельными модулями и подпрограммами, нетрудно, просмотрев основную программу, определить, как работает система в целом.
На рис. 12.7 приведена блок-схема управляющей программы системы, изображенной на рис. 12.6. Отметим, что весь ввод-вывод, вычисления и проверка на наличие ошибок производятся внутри подпрограмм; основная программа лишь определяет последовательность их выполнения.
Тестирование всей системы в целом имеет своей целью обеспечить правильность выполнения работ при допустимых входных данных и выдачу соответствующей диагностики в остальных случаях. Вероятно, хуже всего, если система никак не будет реагировать на неправильные данные. Лучше выдавать сообщения об ошибках в более широком, чем это требуется, классе случаев, чем допустить иллюзию правильной работы при наличии ошибок.
Совершенствование и поддержка
Чаще всего независимо от того, с какой тщательностью были проведены отладка и тестирование, система уже в готовом виде содержит некоторое количество ошибок. Это означает, что оставшиеся ошибки будут обнаруживаться и устраняться параллельно с процессом непосредственного использования системы. Изменения, вносимые в само оборудование или в программное обеспечение, с которым созданный нами пакет используется, часто влекут за собой необходимость внесения изменений в этот уже работающий пакет. Также достаточно часто в уже работающую систему вносятся некоторые дополнения. Поддержка систем, т. е. обеспечение возможности их использования в изменяющихся условиях, достигается посредством все того же модульного программирования и объединения этих модулей в пакеты. Добавление в систему новых возможностей в идеальном случае требует лишь добавления к ней новых модулей (конечно, после их отладки и тестирования).
Рис. 12.7. Блок-схема управляющей программы системы учета запасов.
Рискуя показаться навязчивым, я, тем не менее, снова указываю на необходимость подробного документирования всех программ. Документирование отлаживаемых и тестируемых программ и систем хотя и не является приятным, тем не менее, очень полезное занятие. Отсутствие такого рода документации не дает возможности учесть изменения, внесенные в программу на этапах отладки и тестирования. Документация должна обеспечивать пользователей всей информацией, необходимой для организации поддержки программы и внесения в нее необходимых дополнений.