Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
30
Добавлен:
23.03.2015
Размер:
885.76 Кб
Скачать

8.2.2. Обобщенность

Хорошая спецификация должна быть достаточно обобщенной, позволяя сократить до минимума число исключаемых, однако приемлемых программ.^Важность критерия обобщенности может быть не столь очевидна, как критерия ограниченности.Желательно не только сократить ограничения на число приемлемых реализаций, но и обеспечить возможность использования наиболее удачных (более эффективных или «элегантных») из них. Например, спецификация

sqrt = proc (sq: real, e: real) returns (root: real)

requires sq^O & e;> .001. effects 0 ^ (root * root —sq) ^ e.

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

Желательно создавать спецификации максимально обобщен­ными. Это привело нас к «дефинитивному» стилю, который, и ис­пользован в данной книге.у Такая, спецификация яйй^Ь^ечис-ля^ свойства, которыми должны обладать спецификаторы. Аль­тернативой дефинитивной спецификации является операционная спецификация^Вместо описания свойств спецификаторов опера­ционная спецификация приводит способ конструирования их.

Например,

search = proc (a: array [int], х: int) returns (i: int) signais (not.in) effects Анализирует a[low], a[low+ 1],...,поочгредно ii возвращает индекс элемента, равного х. Сигнализирует not-in, если ни один из эле­ментов не равен х.

есть операционная спецификация операции serach, в то время как

search = proc (a: array [int], х: int) returns (i: int) signals (not-in) effects Возвращает i, такое, что a[i] = х; х. Сигнализирует not_in, если такое i отсутствует.

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

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

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

8.2.3. Простота

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

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

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

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

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

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

р = proc(sl, s2: set) returns (b: bool)

effects b имеет значение true, если si есть подмножество s2, и false —в про­тивном случае.

p = proc(sl, s2: set) returns (b: bool)

effects b = yx [x ^ si имплицирует x ^ s2].

p = proc (si, s2: set) returns (b: bool)

effects b имеет значение true, если si есть подмножество s2, и false —в пр»« тивном случае, т.е. b = ух [х (- si имплицирует х ^ s2].

Первая спецификация кратка и вполне понятна большинству читателей. Однако некоторые из них могут задаться вопросом: было ли понятие «подмножество» выбрано достаточно продуманно или автор имел в виду соответствующее подмножество? Вторая спецификация, которую прочесть чуть сложнее, не оставляет ни­каких сомнений по этому вопросу. Возникает другой вопрос; почему составитель спецификации, полагая процедуру р провер­кой на наличие подмножества, не указал это явно? Третья спе­цификация отвечает на оба этих вопроса.

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

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

pv = proc (inc, r: real, n: int) returns (value: real)

requires inc > 0 & r •> 0 & n :> 0.

effects Возвращает имеющееся значение годового дохода для inc за пе­риод n лет в проценте прироста без риска величиной г.

То есть value == inc+ (inc/(l +г)+...+ (inc/(l + г)"~'). Например, pv (100, .10,3) = 100+ 100/1.1 + 100/1.21

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

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

Избыточность не сокращает число ошибок в спецификации. Она делает их более очевидными и дает возможность читателю их обнаружить. Рассмотрим, например,

too.cold = proc (temp: int) returns (b: bool)

effects b = true, если temp < 0 градусов по Фаренгейту; в противном случае b = false.

too.cold = proc (temp; int) returns (b: bool)

effects b = true, если temp < 0 градусов по Фаренгейту; в противном случае b = false. То есть b == true в точности в том случае, когда temp не больше, чем точка замерзания воды при нормальной температуре и давлении.

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

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

billion = proc ( ) returns (b: int)

effects Возвращает целое число значением в один биллион.

billion = proc ( ) returns (b: int)

effects Возвращает целое число значением в один биллион, т. е. 10°.

Как американские, так и английские читатели найдут первую спе­цификацию абсолютно безошибочной. К сожалению, они интер­претируют ее совершенно по-разному, поскольку в Соединенных Штатах биллион есть 10", а в Великобритании биллион есть 10".

8.3. Почему именно спецификации?

Спецификации важны для достижения требуемой модульности программы. Абстракция используется для декомпозиции про­граммы на модули. Однако взятая в отдельности абстракция яв­ляется малопонятной. Без какого-либо описания мы не можем ни сказать, что она из себя представляет, ни отличить ее от одной из своих реализаций. В качестве такого описания и выступает спецификация.

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

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

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

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

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

Однажды написанные, спецификации могут служить различ­ным целям. Они одинаково полезны проектировщикам, разработ­чикам и лицам, сопровождающим математическое обеспечение. В течение фазы реализации программного обеспечения наличие хорошей спецификации помогает как реализующим заданный мо­дуль, так и тем, кто использует затем этот модуль. Как уже гово­рилось, хорошая^^спецификация поддерживает баланс между ограниченностью и обобщенностью. Она сообщает разработчику, какие функции необходимо реализовать, однако не наклады­вает излишних ограничений на способ их реализации. Это дает разработчику максимальную свободу в написании модулей, согла­сованную в то же время с требованиями пользователей. Разу­меется, спецификации существенны и для пользователей, которым не на что больше положиться при реализации своих модулей. Кроме спецификаций имеются только лишь тексты программ, а на текущий момент обычно неизвестно, какая часть программы претерпела изменения. В процессе тестирования спецификации предоставляют информацию, которая может быть использована для генерации тестировочных данных и построения заглушек, имитирующих работу данного модуля. (Мы обсудим это применение спецификации в гл. 9.) На этапе компоновки системы на­личие хороших спецификаций позволяет сократить число и серь­езность проблем, связанных с интерфейсами, за счет уменьшения числа различных неявных предположений об этих интерфейсах. При обнаружении ошибки спецификации позволяют выявить' их местоположение. Более того, они определяют ограничения, которые необходимо соблюсти при исправлении ошибки, что помогает избежать новых ошибок в процессе исправления ста­рых.

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

8.4. Заключение

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

Хорошие спецификации должны быть ограниченными, обоб­щенными и простыми. Ограниченность и обобщенность предпола­гает наличие набора модулей, удовлетворяющих спецификации: не допускаются реализации, неприемлемые для пользователей абстракции, а предпочтительные реализации (например, более эффективные) должны быть выделены. Обобщенность гораздо легче реализовать в том случае, если спецификации написаны с ис­пользованием дефинитивного подхода, который только задает свойства набора спецификаторов. Операционный подход, объяс­няющий способ реализации абстракции, дает слишком ограничен­ные спецификации.

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

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

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

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

Дополнительная литература

Parnas, David L,, 1977, The use of precise specifications in the development of software. In Proceedings of IFIP Congress 77, pp. 861—868.

Упражнения

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

8.2. Воспользовавшись спецификацией, рассмотренной в предыдущей главе, проанализируйте ее ограниченность, обобщенность и простоту. 8.3. Разумно ли ставить вопрос о корректности спецификации? Объясните. 8.4. Обсудите, как спецификации могут быть использованы в процессе компоновки системы. 8.5. Рассмотрите взаимосвязь абстракции, ее спецификации и реализации.

Тестирование и отладка

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

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

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

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

Альтернативой верификации служит тестирование. Легко можем убедиться в том, что для некоторого набора входных зна­чений программа работает правильно, простой проверкой ее ра­боты с этими значениями. Если набор возможных входных зна­чений невелик, то возможна полная проверка (для каждого из входных значений). Но для большинства программ входной на­бор слишком велик (зачастую бесконечен) и полная проверка не представляется возможной. Однако грамотно подобранный на­бор проверочных значений может в большой степени укрепить нашу уверенность в том, что программа функционирует пра­вильно. Хорошо произведенный тест позволяет выявить боль­шинство ошибок в программах.

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

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

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

Успех тестирования заключается в выборе подходящего на­бора проверочных данных. Как уже говорилось, исчерпывающее тестирование для большинства программ осуществить невозможно. Например, если программа принимает на входе три целых числа, каждое из которых может изменяться в диапазоне от 1 до 1000, то полная проверка потребует выполнения программы миллион раз. Если каждый прогон программы занимает по времени одну секунду, то на полную проверку потребуется немногим более 31 года.

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

9.1.1. Тестирование методом черного ящика

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

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

sqrt = proc (х: real, epsilon: real) returns (ans: real)

requires х >= 0 & (00001 < epsilon < .001)

effects (х — epsilon <= ans * ans <= x + epsilon)

В приведенной спецификации предложение requires представляет собой конъюнкцию двух термов:

1. х>=0

2. (.00001 < epsilon < .001).

Для выделения условий, при которых предложение requires будет удовлетворено, мы должны проверить парные комбинации путей, при которых должно удовлетворяться каждое из условий. Поскольку первое условие есть дизъюнкция двух примитивных термов (х >- 0 есть сокращение от х =•= 0 1 х> 0), то оно может быть удовлетворено двумя способами. Это оставляет нам ситуа­ции, при которых требования в предложении requires удовлетво­ряются:

1. х = 0 & .00001 < epsilon < .001.

2. х > 0 & .00001 < epsilon < .001.

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

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

Тем не менее мы должны внимательно проанализировать предложение effects и попытаться отыскать данные, которые ему удовлетворяют. Например, рассмотрим операцию intset$member:

member = proc (s: intset, х: int) returns (bool) effects Возвращает значение true, если х принадлежит s; в противном

случае возвращается значение false.

Предложение effects в этой спецификации представляет собой конъюнкцию: либо х принадлежит s, либо нет.

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

search^ proc (a: array lint], х: int) returns': int) signals (notJn) •-' effects Если х не принадлежит а, то возвращается i, такое, что a [i]=x;

в противном случае — сообщение not-in.

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

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

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

1. Логические ошибки, при которых обработка граничного условия не приводит к выходу на специальную подпрограмму его обработки, и

2. Отсутствие проверки условий, которые могут привести к возникновению исключительной ситуации в языке, расположен­ном на более низком уровне, или в аппаратной части (например, арифметическое переполнение).

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

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

append, array -= proc (a I, a2: array [int])

modifies al и a2

effects Удаляет элементы из a2 и помещает их в конец al

которая была реализована следующим образом)

append.array == proc (al, a2: array [int])

ai = array [int]

while ai$size (a2) > 0 do

ai$addh (al, a2 [ai$low(a2)])

ai$reml (a2)

end

end append, array

Любые проверочные значения, у которых al и a2 связаны с од­ним и тем же непустым массивом, приведут к возникновению в про­цедуре append.array серьезной ошибки.

9.1.2. Тестирование на основании текста программы

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

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

max_of_three == proc (х, у, z: int) returns (int) if x>y

then if x > z

then return (х) else return (z) end

else if у > z then return (y) else return (z) end

end

end max_of three

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

3,2,4 1,2,1 1,2,3

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

mах. of. three == proc (х, у, z: int) returns (int) return (х) end max. of. three

Набор для теста содержит только тройку

2,1,1

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

Другая потенциальная проблема, связанная со стратегией тестирования, основанной на выборе полномаршрутной проверки, заключается в наличии порой слишком большого числа маршру­тов. Это делает ее непрактичной. Рассмотрим фрагмент программы на рис. 9.1. Как видно из последующего анализа, в этой программе имеется У"" маршрутов. Оператор if обусловливает выполнение одной или другой ветви программы, и оба этих маршрута приво­дят к выполнению следующего прохода цикла. Следовательно, для каждого прохода через i-ю итерацию имеется два прохода через (i + 1)-ю итерацию. Поскольку к первой итерации ведет только один маршрут, то число маршрутов по выходу из i-й ите­рации составляет 2i. Следовательно, из 100-й итерации существует 2100 выхода.

j:=k

for i: int in int$fromJo (1, 100) do

if pred (i *j) then j :== j + I end

end

Рис. 9.1. Программа с большим числом маршрутов.

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

j:=k

for i: int in int$from_to (1, 2) do

if pred (i *j) then j :== j + 1 end

end

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

1. pred (k) pred (2k + 2)

2. pred (k) ~pred (2k + 2)

3. ~pred (k) pred (2k)

4. ~pred (k) ~pred (2k)

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

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

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

while х S> 0 do

% некоторые действия end

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

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

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

Соседние файлы в папке Б. Лисков