Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 22 Тестирование, выполняемое разработчиками 501 затель, соответствующая ему область памяти может быть освобождена. Если это индекс цикла for, после выполнения цикла он может остаться за пределами текущей области видимости. Если это указатель на запись в файле, он может стать недействительным в результате закрытия файла. В дополнение к терминам «определение», «использование» и «уничтожение» удобно иметь термины, описывающие выполнение какого#то действия над переменной сразу после входа в метод или непосредственно перед выходом из него. Вход — поток управления входит в метод, после чего над переменной сразу выполняется какое#то действие. Пример — инициализация рабочей перемен# ной в начале метода. Выход — поток управления покидает метод сразу после выполнения опера# ции над переменной. Пример? Присвоение возвращаемого значения перемен# ной статуса в самом конце метода. Изменения состояния данных Нормальный жизненный цикл переменной предполагает, что переменная опре# деляется, используется один или несколько раз и, возможно, уничтожается. Ука# занные ниже варианты должны вызывать у вас подозрение. Определение — определение Если вы должны дважды определить перемен# ную, чтобы она получила свое значение, вам следует улучшить не программу, а компьютер! Даже если эта комбинация не является неверной, она впустую тратит ресурсы и подвержена ошибкам. Определение — выход Если речь идет о локальной переменной, нет смыс# ла ее определять, если вы покидаете метод, не использовав ее. Если это пара# метр метода или глобальная переменная, возможно, все в порядке. Определение — уничтожение Определение переменной и ее уничтожение указывает на лишнюю переменную или на отсутствие кода, который должен был ее использовать. Вход — уничтожение Это проблема, если переменная является локальной. Переменную нельзя уничтожить, если до этого она не была определена или использована. Если, с другой стороны, речь идет о параметре метода или гло# бальной переменной, эта комбинация нормальна, но только если ранее пере# менная была определена где#то в другом месте. Вход — использование Опять#таки, если переменная является локальной, это проблема. Чтобы переменную можно было использовать, ее нужно опре# делить. Если мы имеем дело с параметром метода или глобальной переменной, все нормально, но только если ранее переменная была где#то определена. Уничтожение — уничтожение Переменные не требуют двойного уничто# жения: они не воскресают. Воскресшая переменная — признак небрежного про# граммирования. Кроме того, двойное уничтожение губительно в случае указа# телей: трудно найти более эффективный способ подвесить компьютер, чем двойное уничтожение (освобождение) указателя. Уничтожение — использование Вызов переменной после уничтожения — логическая ошибка. Если код все же работает (например, указатель все еще ука# зывает на освобожденную область памяти), это лишь случайность, и, по одно# 502 ЧАСТЬ V Усовершенствование кода му из законов Мерфи, программа потерпит крах тогда, когда это причинит наибольший ущерб. Использование — определение Определение переменной после использо# вания может быть проблемой, а может и не быть. Это зависит от того, была ли переменная также определена до ее использования. Если вы встречаете такую комбинацию, обязательно проверьте наличие предшествующего определения. Проверьте код на предмет этих аномальных изменений состояния данных до начала тестирования. После их проверки написание тестов, основанных на потоке дан# ных, сводится к анализу всех возможных комбинаций «определение — использо# вание». Выполнить это можно с различной степенью тщательности. Проверка всех определений. Протестируйте каждое определение каждой пе# ременной, т. е. все места, в которых какая#либо переменная получает свое зна# чение. Это слабая стратегия, поскольку, если вы тестируете каждую строку кода, вы и так выполните это. Проверка всех комбинаций «определение — использование». Протестируйте все комбинации, включающие определение переменной в одном месте и вы# зов в другом. Это более грамотная стратегия, так как простое тестирование каждой строки кода далеко не всегда обеспечивает проверку всех комбинаций «определение — использование». Вот пример: Пример программы, поток данных которой мы протестируем (Java) if ( Condition 1 ) { x = a; } else { x = b; } if ( Condition 2 ) { y = x + 1; } else { y = x 1; } Для покрытия каждого пути выполнения этой программы нам нужен один тест, при котором условие Condition 1 истинно, и один — при котором оно ложно, а также аналогичные тесты для условия Condition 2. Эти ситуации можно охватить двумя тестами: (Condition 1=True, Condition 2=True) и (Condition 1=False, Condition 2=False). Два этих теста — все, что нужно для выполнения требований структури# рованного базисного тестирования, а также для тестирования всех определений переменных; эти тесты автоматически обеспечивают слабую форму тестирования, основанного на потоках данных. Однако для покрытия всех комбинаций «определение — использование» этого мало. На текущий момент у нас есть тесты тех случаев, когда условия Condition 1 и Condition 2 истинны и когда оба они ложны: ГЛАВА 22 Тестирование, выполняемое разработчиками 503 x = a y = x + 1 и x = b y = x 1 Но у нас есть еще две комбинации «определение — использование»: (1) x = a, после чего y = x % 1 и (2) x = b, после чего y = x + 1. В данном примере эти комбинации можно покрыть, добавив еще два теста: (Condition 1=True, Condition 2=False) и (Condition 1=False, Condition 2=True). Можно порекомендовать следующую стратегию разработки тестов: начните со структурированного базисного тестирования, которое охватит некоторые, если не все потоки данных «определение — использование». После этого добавьте те# сты, нужные для полного охвата комбинаций «определение — использование». Структурированное базисное тестирование указало нам на шесть тестов метода, для которого мы подсчитывали число тестов. Тестирование каждой пары «опре# деление — использование» требует нескольких дополнительных тестов. Вот пары, которые не охватываются уже созданными тестами: Номер теста Описание теста 7 Определение переменной companyRetirement в строке 12 и использо# вание в строке 26. Этот случай может не покрываться имеющимися тестами. 8 Определение переменной companyRetirement в строке 12 и использо# вание в строке 31. Этот случай может не покрываться имеющимися тестами. 9 Определение переменной companyRetirement в строке 17 и использо# вание в строке 31. Этот случай может не покрываться имеющимися тестами. Выполнив тестирование, основанное на потоках данных, несколько раз, вы нач# нете чувствовать, какие тесты нужны, а какие уже реализованы. В случае сомне# ний ищите все комбинации «определение — использование». Это может показаться слишком объемной работой, но так вы точно найдете все случаи, не охваченные базисным тестированием. Разделение на классы эквивалентности Хороший тест должен покрывать широкий диапазон вход# ных данных Если два теста приводят к обнаружению одних и тех же ошибок, вам нужен лишь один из них. Понятие «разделения на классы эквивалентности» формализует эту идею и помогает уменьшить число нужных тестов. Подходящее место для разделения на классы эквивалентно# сти — строка 7 уже известного нам листинга, в которой проверяется условие m_employee[ ID ].governmentRetirementWithheld < MAX_GOVT_RETIREMENT. Это условие Перекрестная ссылка Разделе- ние на классы эквивалентнос- ти гораздо подробнее обсужда- ется в книгах, указанных в раз- деле «Дополнительные ресур- сы» в конце этой главы. 504 ЧАСТЬ V Усовершенствование кода делит все значения m_employee[ ID ]. governmentRetirement Withheld на два класса эквивалентности: значения, которые меньше константы MAX_GOVT_RETIREMENT, и значения, которые больше или равны ей. В других частях программы могут использоваться другие классы эквивалентности, что может потребовать тестиро# вания более двух значений m_employee[ ID ].governmentRetirementWithheld, но в этой части программы нужно проверить только два значения. Размышление о разделении на классы эквивалентности не скажет вам много но# вого о программе, если вы уже покрыли ее базисным тестированием и тестиро# ванием, основанным на потоках данных. Однако оно очень полезно, если вы смот# рите на программу «извне» (с точки зрения спецификации, а не исходного кода) или если данные сложны, а эта сложность плохо отражена в логике программы. Угадывание ошибок Формальные методики тестирования хорошие программи# сты дополняют менее формальными эвристическими мето# диками. Одна из них — угадывание ошибок (error guessing). Термин «угадывание ошибок» — довольно примитивное название вполне разум# ной идеи, подразумевающей создание тестов на основе обдуманных предположе# ний о вероятных источниках ошибок. Предположения можно выдвигать, опираясь на интуицию или накопленный опыт. Так, в главе 21 в числе прочих достоинств инспекций были указаны создание и обновление списка частых ошибок, используемого при проверке нового кода. Поддерживая списки ранее допущенных ошибок, вы повысите эффективность своих догадок. Ниже рассматриваются конкретные виды ошибок, угадать которые легче всего. Анализ граничных условий Одной из самых плодотворных областей тестирования являются граничные ус# ловия — ошибки занижения или завышения на 1. Действительно, разработчики очень часто используют num – 1 вместо num или >= вместо >. Идея анализа граничных условий состоит в написании тестов, позволяющих про# верить эти условия. Так, при тестировании диапазона значений, которые меньше max, возможны три условия: Как видите, в этой ситуации мы имеем три граничных случая: максимальное зна# чение, которое меньше max, само значение max и минимальное значение, пре# вышающее max. Для исключения распространенных ошибок нужны три теста. Фрагмент кода, для которого мы подсчитывали число тестов, содержит проверку m_employee[ ID ].governmentRetirementWithheld < MAX_GOVT_RETIREMENT. Согласно принципам анализа граничных условий следует изучить три случая: Перекрестная ссылка Об эвристи- ческих методиках см. раздел 2.2. ГЛАВА 22 Тестирование, выполняемое разработчиками 505 Номер теста Описание теста 1 Тест требует, чтобы истинное значение выражения m_employee[ ID ]. governmentRetirementWithheld < MAX_GOVT_RETIREMENT было первым с истинной стороны границы. Иначе говоря, мы должны присвоить элементу m_employee[ ID ].governmentRetirementWithheld значение MAX_GOVT_RETIREMENT % 1. Для этого годится уже имеющийся тест 1. 3 Тест требует, чтобы ложное значение выражения m_employee[ ID ]. go vernmentRetirementWithheld < MAX_GOVT_RETIREMENT было первым с ложной стороны границы. Таким образом, элементу m_employee[ ID ].governmentRetirementWithheld нужно присвоить значение MAX_GOVT_ RETIREMENT + 1. Для этого вполне подойдет тест 3. 10 Дополнительный тест нужен для самой границы, когда m_employee [ ID ].governmentRetirementWithheld = MAX_GOVT_RETIREMENT. Сложные граничные условия Анализ граничных условий можно проводить также в отношении минимального и максимального допустимых значений. В нашем примере ими могли бы быть минимальные или максимальные значения переменных grossPay, companyRetirement или personalRetirement, но из#за того, что эти значения вычисляются вне области видимости метода, их тестирование мы обсуждать не будем. Более тонкий вид граничного условия имеет место, когда оно зависит от комби# нации переменных. Например, что произойдет при умножении двух переменных, если обе являются большими положительными числами? Большими отрицатель# ными числами? Если хотя бы одна из переменных равна 0? Что, если все пере# данные в метод строки имеют необычно большую длину? В текущем примере вы могли бы проверить, что происходит с денежными сум# мами, которые представлены переменными totalWithholdings, totalGovernment% Retirement и totalRetirement, если каждый член большой группы имеет крупную зарплату — скажем, каждый из программистов зарабатывает по 250 000 долларов (надежда умирает последней!). Для этого нужен еще один тест: Номер теста Описание теста 11 Большая группа высокооплачиваемых сотрудников (конкретные пока# затели зависят от конкретной системы — скажем, 1000 сотрудников, каждый из которых зарабатывает по 250 000 долларов в год), не вы# плачивающих взносы в фонд социального страхования, но отчисляю# щих средства в пенсионный фонд компании. Противоположным тестом из этой же категории было бы вычисление всех пока# зателей для небольшой группы сотрудников, не получающих зарплату: Номер теста Описание теста 12 Группа из 10 сотрудников, не получающих зарплату. Классы плохих данных Кроме граничных условий, программу можно тестировать на предмет несколь# ких других классов плохих данных. Типичными примерами плохих данных мож# но считать: 506 ЧАСТЬ V Усовершенствование кода недостаток данных (или их отсутствие); избыток данных; неверный вид данных (некорректные данные); неверный размер данных; неинициализированные данные. Некоторые из этих случаев уже покрыты имеющимися тестами. Так, «недостаток данных» охватывается тестами 2 и 12, а для «неверного размера данных» тесты придумать трудно. И все же рассмотрение классов плохих данных позволяет со# здать еще несколько тестов: Номер теста Описание теста 13 Массив из 100 000 000 сотрудников. Тестирование на предмет избытка данных. Конечно, объем данных, который следует считать избыточ# ным, зависит от конкретной системы. 14 Отрицательная зарплата. Неверный вид данных. 15 Отрицательное число сотрудников. Неверный вид данных. Классы хороших данных При поиске ошибок легко упустить из виду тот факт, что номинальный случай так# же может содержать ошибку. Примерами хороших данных могут служить номиналь# ные случаи, описанные в разделе, посвященном базисному тестированию. Ниже ука# заны другие виды хороших данных, которые стоит подвергать проверке; проверка каждого из этих видов данных также может приводить к обнаружению ошибок: номинальные случаи: средние, ожидаемые значения; минимальная нормальная конфигурация; максимальная нормальная конфигурация; совместимость со старыми данными. Минимальную нормальную конфигурацию полезно применять для тестирования не только одного элемента, но и группы элементов. Минимальная нормальная конфигурация аналогична граничному условию, составленному из нескольких минимальных значений, но отличается от него тем, что она включает набор ми# нимальных значений из диапазона нормально ожидаемых значений. В качестве примера можно привести сохранение пустой электронной таблицы. При тести# ровании текстового редактора примером могло бы быть сохранение пустого до# кумента. В нашем примере тестирование минимальной нормальной конфигура# ции добавило бы в набор следующий тест: Номер теста Описание теста 16 Группа из 1 сотрудника. Тестирование минимальной нормальной конфигурации. Максимальная нормальная конфигурация противоположна минимальной. Она также аналогична граничному условию, но опять#таки включает набор максималь# ных значений из диапазона ожидаемых значений. Пример — сохранение или ГЛАВА 22 Тестирование, выполняемое разработчиками 507 печать электронной таблицы, имеющей «максимальный размер», заявленный в рекламных материалах. В случае текстового процессора — сохранение докумен# та максимального рекомендованного размера. У нас максимальная нормальная конфигурация определяется максимальным нормальным числом сотрудников. Если бы оно равнялось 500, вы добавили бы в набор такой тест: Номер теста Описание теста 17 Группа из 500 сотрудников. Тестирование максимальной нормальной конфигурации. Последний вид тестирования нормальных данных — тестирование совместимо# сти со старыми данными — вступает в игру при замене старого приложения или метода на новый вариант. При вводе старых данных новый метод должен выда# вать те же результаты, что и старый метод, за исключением тех ситуаций, в кото# рых старый метод работал ошибочно. Этот вид преемственности версий лежит в основе регрессивного тестирования, призванного гарантировать, что исправле# ния и улучшения поддерживают достигнутый уровень качества, не вызывая про# блем. В нашем примере критерий совместимости не добавил бы никаких тестов. Используйте тесты, позволяющие легко проверить результаты вручную Допустим, вы пишете тест для проверки расчета зарплаты; вам нужно ввести зарпла# ту, и один из способов сделать это — ввести числа, которые попадаются под руку. Попробуем: 1239078382346 Отлично. Это довольно большая зарплата — более триллиона долларов, но если ее «обрезать», можно получить что#то более реалистичное: скажем, 90 783,82 доллара. Теперь предположим, что этот тест успешен, т. е. указывает на ошибку. Как узнать, что вы обнаружили ошибку? Ну, вы можете вычислить правильный результат вруч# ную и сравнить его с результатом, полученным на самом деле. Однако если в вы# числениях фигурируют такие неприятные числа, как 90 783,82 доллара, вероят# ность допустить ошибку в вычислениях не менее высока, чем вероятность обна# ружения ошибки в программе. С другой стороны, удобные круглые числа вроде 20 000 долларов делают вычисления сущим пустяком. Нули легко набирать, а ум# ножение на 2 большинство программистов способны выполнять в уме. Возможно, вы считаете, что неудобные числа чаще приводят к обнаружению ошибок, но это не так: при использовании любого числа из одного и того же класса эквивалентности вероятность нахождения ошибки одинакова. 22.4. Типичные ошибки Главная идея этого раздела в том, что для достижения максимальной эффективно# сти тестирования мы должны как можно больше знать о нашем враге — ошибках. 508 ЧАСТЬ V Усовершенствование кода Какие классы содержат наибольшее число ошибок? Естественно предположить, что дефекты распределяются по коду равно# мерно. Если код содержит в среднем 10 дефектов на 1000 строк, вы мог# ли бы ожидать, что класс из 100 строк будет иметь один дефект. Это ес# тественное предположение, но оно ошибочно. Кейперс Джонс сообщает, что в результате принятой в IBM программы повыше# ния качества 31 из 425 классов системы IMS получил статус «подверженный ошиб# кам». Эти классы были исправлены или полностью разработаны заново, благода# ря чему менее чем через год частота обнаружения дефектов в IMS клиентами сни# зилась в 10 раз. Общие расходы на сопровождение системы снизились пример# но на 45%. Мнения клиентов о качестве системы изменились с «неприемлемо» на «хорошо» (Jones, 2000). Большинство ошибок обычно концентрируется в нескольких особенно дефект# ных методах. Типичные отношения между ошибками и кодом таковы: 80% ошибок содержится в 20% классов или методов проекта (Endres, 1975; Gremillion, 1984; Boehm, 1987b; Shull et al., 2002); 50% ошибок содержится в 5% классов проекта (Jones, 2000). Эти отношения могут казаться не такими уж и важными, пока вы не узнаете не# сколько следствий. Во#первых, 20% методов проекта обусловливают 80% затрат на разработку (Boehm, 1987b). Это не значит, что 20% самых дорогих методов явля# ются одновременно и самыми дефектными, но такое совпадение настораживает. Во#вторых, какой бы ни была точная доля расходов, приходящихся на разработку дефектных методов, эти методы очень дороги. В классичес# ком исследовании, проведенном в 1960#х, специалисты IBM проанали# зировали операционную систему OS/360 и обнаружили, что ошибки не были рас# пределены равномерно между всеми методами, а были сконцентрированы в не# скольких методах. Был сделан вывод, что эти методы — «самые дорогие сущнос# ти в программировании» (Jones, 1986a). Они содержали целых 50 дефектов на 1000 строк кода, а их исправление часто оказывалось в 10 раз дороже разработки всей системы (затраты включали поддержку пользователей и сопровождение системы на месте). В#третьих, дорогие методы оказывают очевидное влияние на процесс разработки. Как гласит старая пословица, «вре# мя — деньги». Справедливо и обратное: «деньги — время», и, если вы можете исключить почти 80% затрат, избежав проблемных методов, вы можете сэкономить и много вре# мени. Это наглядно иллюстрирует Главный Закон Контро# ля Качества ПО: повышение качества сокращает сроки и снижает общую стоимость разработки системы. В#четвертых, проблемные методы оказывают не менее очевидное влияние на со# провождение программы. При сопровождении программистам приходится сосре# доточиваться на идентификации методов, подверженных ошибкам, их перепро# ектировании и переписывании с нуля. В вышеупомянутом проекте IMS после за# мены дефектных классов производительность труда при разработке новых вер# сий IMS повысилась примерно на 15% (Jones, 2000). Перекрестная ссылка Другим типом методов, часто содержа- щих много ошибок, являются слишком сложные методы. Об идентификации таких методов и их упрощении см. подраздел «Общие принципы уменьшения сложности» раздела 19.6. |