Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 22 Тестирование, выполняемое разработчиками 493 ное изучение ошибочных выходных данных. Ошибки были видны, но програм# мисты их не заметили (Myers, 1978). Вы должны надеяться обнаружить ошибки в своем коде. Это может казаться неес# тественным, но лучше уж найти свои ошибки самому, иначе вам на них укажет кто#то другой. Один из важнейших вопросов состоит в том, сколько времени разработчикам следует уделять тестированию в типичном проекте. Часто говорят, что на все те# стирование уходит 50% времени разработки системы, но это может ввести в за# блуждение. Во#первых, это число охватывает и тестирование, и отладку; само же тестирование занимает меньше времени. Во#вторых, это число отражает время, которое обычно тратят на тестирование, а не время, которое следует тратить. В#третьих, это число включает и тестирование, выполняемое разработчиками, и независимое тестирование. В зависимости от объема и сложности проекта тестированию, выполняемому разработчиками, вероятно, следует посвящать от 8 до 25% общего времени рабо# ты над проектом. Это согласуется с данными многих авторов (рис. 22#1). Рис. 22'1. По мере увеличения объема проекта тестирование, выполняемое разработчиками, отнимает все меньшую часть общего времени разработки. О влиянии размера программы на конструирование см. главу 27 Другой важный вопрос: что делать с результатами тестирования, выполняемого разработчиками? Прежде всего вы можете сразу использовать эти результаты для оценки надежности разрабатываемой системы. Даже если вы не исправляете де# фекты, обнаруженные при тестировании, оно характеризует надежность ПО. Кроме того, результаты тестирования позволяют определить необходимые исправления. Наконец, со временем регистрация дефектов, обнаруженных при тестировании, помогает определить наиболее частые типы ошибок. Вы можете использовать эту информацию для выбора и проведения подходящих обучающих курсов, при под# готовке будущих технических обзоров и разработке будущих тестов. Тестирование во время конструирования В большом мире тестирования тему этой главы — тестирование методом белого (или прозрачного) ящика — иногда игнорируют. Как правило, проектировать 494 ЧАСТЬ V Усовершенствование кода классы следует так, чтобы они казались черными ящиками: пользователь класса должен обращаться к нему через интерфейс, не зная о деталях внутреннего уст# ройства класса. Однако при тестировании класса с ним выгодно обращаться как с прозрачным ящиком, анализируя внутренний исходный код, а также входные и выходные данные класса. Зная, что происходит внутри ящика, вы можете проте# стировать класс более тщательно. Конечно, при тестировании класса вы будете рассматривать его код под тем же углом зрения, что и при написании, поэтому тестирование методом черного ящика также имеет достоинства. Во время конструирования вы обычно пишете метод или класс, проверяете его в уме, после чего выполняете его обзор или тестирование. Какой бы ни была ваша стратегия интеграционного тестирования или тестирования системы, вы долж# ны тщательно тестировать каждый блок до его объединения с другими блоками. Если вы пишете несколько методов, тестируйте их по одному за раз. На самом деле так их тестировать не легче — зато гораздо легче отлаживать. Если вы объедини# те несколько непротестированных методов и получите ошибку, вы не сможете определить, какой из методов в ней повинен. Если же вы добавляете в набор про# тестированных методов по одному методу за раз, при возникновении ошибки вы будете знать, что она содержится в добавленном методе или обусловлена взаимо# действием старых методов с новым. Это облегчит отладку. Методики совместного конструирования имеют много достоинств, не характер# ных для тестирования. Однако частично это объясняется тем, что тестирование часто выполняют не так хорошо, как было бы можно. Разработчик может выпол# нить сотни тестов и все же достигнуть лишь частичного покрытия кода тестами. Чувство хорошего покрытия кода тестами не означает, что покрытие на самом деле адекватно. Понимание базовых концепций тестирования может повысить его эффективность. 22.2. Рекомендуемый подход к тестированию, выполняемому разработчиками Систематичный подход к тестированию, выполняемому разработчиками, позво# ляет находить максимальное число дефектов всех типов при минимуме усилий. Поэтому соблюдайте все правила, указанные в следующем списке. Тестируйте программу на предмет реализации каждого существенного требо# вания. Планируйте тесты для этого этапа на стадии выработки требований или как можно раньше — лучше всего до написания тестируемого блока. Подумайте о тестах, которые позволили бы выявить распространенные пробелы в требо# ваниях. Уровень безопасности, хранение данных, процедура установки и на# дежность системы — все эти области тестирования часто упускаются на этапе выработки требований. Тестируйте программу на предмет реализации каждого значимого аспекта проектирования. Планируйте тесты для этого этапа на стадии проектирования или как можно раньше — до начала детального кодирования метода или клас# са, подлежащего тестированию. ГЛАВА 22 Тестирование, выполняемое разработчиками 495 Используя «базисное тестирование», дополните тесты требований и проекта детальными тестами. Разработайте тесты, основанные на потоках данных, а затем создайте остальные тесты, нужные для тщательного тестирования кода. Как минимум вы должны протестировать каждую строку кода. О базисном те# стировании и тестировании, основанном на потоках данных, см. ниже. Используйте контрольный список ошибок, созданный вами для текущего про# екта или в предыдущих проектах. Проектируйте тесты вместе с системой. Это помогает избегать ошибок в требо# ваниях и проекте, которые обычно дороже ошибок кодирования. Выполняйте тестирование и ищите дефекты как можно раньше, потому что в этом случае ис# правление дефектов будет дешевле. Когда создавать тесты? Разработчики иногда интересуются, когда лучше создавать тесты: до написания кода или после? (Beck, 2003) Из графика повышения стоимости дефектов (рис. 3#1) сле# дует, что предварительное написание тестов позволяет свести к минимуму интервал времени между моментами внесения дефекта и его обнаружения/устранения. Есть и другие мотивы предварительного написания тестов: создание тестов до написания кода требует тех же усилий: вы просто изменя# ете порядок выполнения этих двух этапов; если вы пишете сначала тесты, вы найдете дефекты раньше, да и исправить их легче; предварительное написание тестов заставляет хоть немного задумываться о требованиях и проекте до написания кода, что способствует улучшению кода; предварительное написание тестов позволяет найти проблемы в требованиях до написания кода, потому что трудно создать тест для плохого требования; если вы сохраняете свои тесты, что следует делать всегда, вы можете выпол# нить тестирование и после написания кода. По#моему, программирование с изначальными тестами — одна из самых эффек# тивных методик разработки ПО, возникших в последнее десятилетие. Но это не панацея, потому что такой подход тоже страдает от общих ограничений тести# рования, выполняемого разработчиками. Ограничения тестирования, выполняемого разработчиками Ниже я описал ряд ограничений этого вида тестирования. Разработчики обычно выполняют «чистые тесты» Разработчики склон# ны тестировать код на предмет того, работает ли он (чистые тесты), а не пытать# ся нарушить его работу всевозможными способами (грязные тесты). В организа# циях с незрелым процессом тестирования обычно выполняют около пяти чис# тых тестов на каждый грязный. В организациях со зрелым процессом тестирова# ния на каждый чистый тест обычно приходятся пять грязных. Это отношение из# меняется на противоположное не за счет снижения числа чистых тестов, а за счет создания в 25 раз большего числа грязных тестов (данные Бориса Бейзера [Boris Beizer] в Johnson, 1994). 496 ЧАСТЬ V Усовершенствование кода Разработчики часто имеют слишком оптимистичное представ' ление о покрытии кода тестами Как правило, программисты счи# тают, что они достигают 95%#го покрытия кода тестами, но на самом деле они обычно достигают в лучшем случае примерно 80%#го покрытия, в худшем — 30%#го, а в среднем — где#то на 50–60% [данные Бориса Бейзера (Boris Beizer) в Johnson, 1994]. Разработчики часто упускают из виду более сложные аспекты покры' тия кода тестами Большинство разработчиков считают тип покрытия кода тестами, известный как «100%#е покрытие операторов», адекватным. Это хорошее начало, но такого покрытия едва ли достаточно. Лучший тип покрытия — так называемое «100%#е покрытие ветвей», требующее, чтобы каждой переменной каждого предиката при тестировании было присвоено хотя бы одно истинное и одно ложное значение (подробнее об этом см. раздел 22.3). Эти ограничения не уменьшают важность тестирования, выполняемого разработ# чиками, — они лишь помогают получить о нем объективное представление. Ка# ким бы ценным ни было тестирование, выполняемое разработчиками, его недо# статочно для обеспечения адекватного контроля качества, поэтому его следует дополнять другими методиками, в том числе методиками независимого тестиро# вания и совместного конструирования. 22.3. Приемы тестирования Почему невозможно доказать корректность программы, протестировав ее? Чтобы доказать полную работоспособность программы, вы должны были бы протестиро# вать ее со всеми возможными входными значениями и их комбинациями. Даже в случае самой простой программы такое предприятие оказалось бы слишком мас# штабным. Допустим, ваша программа принимает фамилию, адрес и номер телефо# на, а затем сохраняет их в файле. Очевидно, что это простая программа — гораздо проще, чем те, о корректности которых приходится беспокоиться в действитель# ности. Предположим далее, что фамилии и адреса могут иметь длину 20 символов, каждый из которых может иметь одно из 26 возможных значений. Число возмож# ных комбинаций входных данных было бы следующим: Фамилия 26 20 (20 символов; 26 вариантов каждого символа) Адрес 26 20 (20 символов; 26 вариантов каждого символа) Номер телефона 10 10 (10 цифр; 10 вариантов каждой цифры) Общее число комбинаций = 26 20 * 26 20 * 10 10 » 10 66 Даже при таком относительно небольшом объеме входных данных вам пришлось бы выполнить 10 66 тестов. Если бы Ной, высадившись из ковчега, начал тестиро# вать эту программу со скоростью триллион тестов в секунду, на текущий момент он был бы далек от выполнения даже 1% тестов. Очевидно, что при вводе более реалистичного объема данных исчерпывающее тестирование всех комбинаций стало бы еще менее «осуществимым». ГЛАВА 22 Тестирование, выполняемое разработчиками 497 Неполное тестирование Если уж исчерпывающее тестирование невозможно, на прак# тике искусство тестирования заключается в выборе тестов, способных обеспечить максимальную вероятность обнару# жения ошибок. В нашем случае из 10 66 возможных тестов толь# ко несколько скорее всего позволили бы найти ошибки, от# личающиеся от ошибок, обнаруживаемых другими тестами. Вам нужно выбирать несколько тестов, позволяющих найти разные ошибки, а не использовать множество тестов, раз за разом приводящих к одному результату. Планируя тестирование, исключите тесты, которые не могут сообщить ничего нового, например, тесты новых данных, похожих на уже протестированные дан# ные. Существует ряд методов эффективного покрытия базисных элементов; не# которые из них мы и обсудим. Структурированное базисное тестирование Несмотря на пугающее название, в основе структурированного базисного тести# рования (structured basis testing) лежит довольно простая идея: вы должны проте# стировать каждый оператор программы хотя бы раз. Если оператор является ло# гическим, таким как if или while, вы должны учесть сложность выражения внутри if или while, чтобы оператор был протестирован полностью. Самый легкий спо# соб покрыть все базисные элементы предполагает подсчет числа возможных пу# тей выполнения программы и создание минимального набора тестов, проверяю# щих каждый путь. Возможно, вы слышали о видах тестирования, основанных на «покрытии кода» или «покрытии логики». Эти подходы также требуют тестирования всех путей выполнения программы. В этом смысле они похожи на структурированное базис# ное тестирование, но они не подразумевают покрытия всех путей минимальным набором тестов. Если вы тестируете программу методом покрытия кода или по# крытия логики, вы можете создать для покрытия той же логики гораздо больше тестов, чем при использовании структурированного базисного тестирования. Минимальное число тестов, нужных для базисного тести# рования, найти очень просто: 1. начните с 1 для последовательного пути выполнения метода; 2. прибавьте 1 для каждого из ключевых слов if, while, repeat, for, and и or или их аналогов; 3. прибавьте 1 для каждого блока case; если отсутствует блок, используемый по умолчанию, прибавьте еще 1. Вот пример: Простой пример подсчета числа путей выполнения программы (Java) Начинаем счет: «1» — сам метод. Statement1; Statement2; Перекрестная ссылка Опреде- лить, покрыт ли тестами весь код, позволяет монитор покры- тия (см. соответствующий под- раздел раздела 22.5). Перекрестная ссылка Эта про- цедура похожа на методику оценки сложности (см. подраз- дел «Как измерить сложность» раздела 19.6). > 498 ЧАСТЬ V Усовершенствование кода «2» — оператор if. if ( x < 10 ) { Statement3; } Statement4; В этом случае мы начинаем с 1 и встречаем один оператор if, получая в итоге 2. Это значит, что для покрытия всех путей выполнения этого кода вам нужно со# здать минимум два теста, соответствующих следующим условиям: операторы, контролируемые оператором if, выполняются (x < 10); операторы, контролируемые оператором if, не выполняются (x >= 10). Чтобы у вас сложилось более полное представление об этом виде тестирования, нужно рассмотреть более реалистичный код. В данном случае реализм будет за# ключаться в наличии дефектов. Следующий листинг представляет собой более сложный пример. Он использует# ся во многих частях главы и содержит несколько возможных ошибок. Пример подсчета числа тестов, нужных для базисного тестирования (Java) «1» — сам метод. 1 // Вычисление фактической заработной платы. 2 totalWithholdings = 0; 3 «2» — цикл for. 4 for ( id = 0; id < numEmployees; id++ ) { 5 6 // Вычисление суммы взноса в фонд социального страхования. «3» — оператор if. 7 if ( m_employee[ id ].governmentRetirementWithheld < MAX_GOVT_RETIREMENT ) { 8 governmentRetirement = ComputeGovernmentRetirement( m_employee[ id ] ); 9 } 10 11 // Взнос в пенсионный фонд компании по умолчанию не взимается. > > > > ГЛАВА 22 Тестирование, выполняемое разработчиками 499 12 companyRetirement = 0; 13 14 // Вычисление суммы вклада сотрудника в пенсионный фонд компании. «4» — оператор if; «5» — операция &&. 15 if ( m_employee[ id ].WantsRetirement && 16 EligibleForRetirement( m_employee[ id ] ) ) { 17 companyRetirement = GetRetirement( m_employee[ id ] ); 18 } 19 20 grossPay = ComputeGrossPay ( m_employee[ id ] ); 21 22 // Вычисление суммы вклада на индивидуальный пенсионный счет сотрудника. 23 personalRetirement = 0; «6» — оператор if. 24 if ( EligibleForPersonalRetirement( m_employee[ id ] ) ) { 25 personalRetirement = PersonalRetirementContribution( m_employee[ id ], 26 companyRetirement, grossPay ); 27 } 28 29 // Вычисление фактической заработной платы сотрудника за неделю. 30 withholding = ComputeWithholding( m_employee[ id ] ); 31 netPay = grossPay withholding companyRetirement governmentRetirement – 32 personalRetirement; 33 PayEmployee( m_employee[ id ], netPay ); 34 35 // Добавление вычетов из зарплаты сотрудника в соответствующие фонды. 36 totalWithholdings = totalWithholdings + withholding; 37 totalGovernmentRetirement = totalGovernmentRetirement + governmentRetirement; 38 totalRetirement = totalRetirement + companyRetirement; 39 } 40 41 SavePayRecords( totalWithholdings, totalGovernmentRetirement, totalRetirement); В этом примере нам нужен один первоначальный тест и один тест для каждого из пяти ключевых слов — всего шесть. Это не значит, что шестью любыми теста# ми будут покрыты все базисные элементы. Это значит, что нужно минимум шесть тестов. Если тесты не будут разработаны должным образом, они почти наверняка не покроют все базисные элементы. Хитрость в том, что нужно обращать внима# ние на те же ключевые слова, которые вы использовали при подсчете числа нуж# ных тестов. Все эти ключевые слова представляют нечто, что может быть или истинным, или ложным; убедитесь, что вы разработали хотя бы по одному тесту для каждого истинного и каждого ложного условия. Вот набор тестов, покрывающий все базисные элементы в этом примере: > > 500 ЧАСТЬ V Усовершенствование кода Номер теста Описание теста Данные теста 1 Номинальный случай. Все логические условия истинны 2 Условие цикла numEmployees < 1 for ложно. 3 Условие первого m_employee[ id ].governmentRetirementWithheld оператора if ложно. >=MAX_GOVT_RETIREMENT 4 Условие второго not m_employee[ id ].WantsRetirement оператора if ложно по той причине, что первый операнд операции && ложен. 5 Условие второго not EligibleForRetirement( m_employee[id] ) оператора if ложно по той причине, что второй операнд операции && ложен. 6 Условие третьего not EligibleForPersonalRetirement( m_employee[ id ] ) оператора if ложно. Примечание: позднее мы дополним эту таблицу другими тестами. При возрастании сложности метода число тестов, нужных только для покрытия всех путей, быстро увеличивается. Более короткие методы обычно включают мень# ше путей, которые нужно протестировать. Булевы выражения, не содержащие большого числа операций И и ИЛИ, также имеют меньше вариантов, подлежащих тестированию. Легкость тестирования — еще один хороший повод сокращать методы и упрощать булевы выражения. Итак, мы разработали для нашего метода шесть тестов, выполнив требования струк# турированного базисного тестирования. Можно ли считать, что этот метод пол# ностью протестирован? Наверное, нет. Этот тип тестирования гарантирует толь# ко выполнение всего кода. Он не учитывает вариации данных. Тестирование, основанное на потоках данных Рассмотрев этот и предыдущий подразделы вместе, вы получите еще одно под# тверждение того, что в программировании поток управления и поток данных одинаково важны. Главная идея тестирования, основанного на потоках данных, в том, что исполь# зование данных не менее подвержено ошибкам, чем поток управления. Борис Бейзер утверждает, что по меньшей мере половина всего кода состоит из объяв# лений данных и операций их инициализации (Beizer, 1990). Данные могут находиться в одном из трех состояний, которым соответствуют выполняемые над данными действия, указанные ниже. Определение — данные инициализированы, но еще не использовались. Использование — данные используются в качестве операндов вычислений, аргументов методов или как#нибудь иначе. Уничтожение — определенные когда#то данные в результате некоторых опе# раций становятся недействительными. Например, если данными является ука# |