- •4.9.1. Изменяемость
- •4.9.2. Классы операций
- •4.9.3. Полнота
- •4.9.5. Операции egual, similar и copy
- •5. Исключительные ситуации
- •6.2.1. Сигнализация об исключительных ситуациях
- •5.2.2. Обработка исключительных ситуаций
- •5.2.3. Предложение resignal
- •6.2.4. Необрабатываемые исключительные ситуации
- •6.2.1. Реализация итераторов
- •6.2.2. Использование итераторов
- •7.2. Абстракция данных
- •7.4. Генераторы
- •8.2.2. Обобщенность
- •9.1.3. Пример
- •9.6. Заключение
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 и effects и уточнение их реальной необходимости. Если это не так, то они должны быть исключены или ослаблены. Помимо этого, к любой части спецификации, являющейся более операционной, нежели дефинитивной, следует относиться 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, либо нет.
Часто тестирование, основанное на анализе предложения effects, проверяет функционирование программ обработки ошибок. Отсутствие сигнала о возникновении исключительноя ситуации при запрещенных значениях входных параметров является столь же серьезной ошибкой, что и неверная работа программы при нормальных значениях. Следовательно, данные теста должны проверять все возможные реакции программы. Рассмотрим, например, следующую спецификацию:
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. Для рекурсивных процедур добавляются тесты для тех случаев, когда возврат из процедуры происходит без рекурсивных обращений, а также тесты, рассчитанные только на одно рекурсивное обращение.
Рассмотренное приближение к полномаршрутному тестированию, разумеется, далеко от идеала. Оно часто позволяет обнаружить ошибки, однако не дает никаких полных гарантий.