Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 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). Данные могут находиться в одном из трех состояний, которым соответствуют выполняемые над данными действия, указанные ниже. 쐽 Определение — данные инициализированы, но еще не использовались. 쐽 Использование — данные используются в качестве операндов вычислений, аргументов методов или как-нибудь иначе. 쐽 Уничтожение — определенные когда-то данные в результате некоторых опе- раций становятся недействительными. Например, если данными является ука- ГЛАВА 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 истинны и когда оба они ложны: |