Программист-прагматик. Путь от подмастерья к мастеру
Шрифт:
Когда мы объединим наши "программные интегральные схемы" в единую систему, мы будем уверены, что ее отдельные части работают предсказуемо, а затем можем применить те же средства модульного тестирования при проверке системы в целом. О подобном крупномасштабном тестировании речь идет в разделе "Безжалостное тестирование".
Но прежде чем выйти на этот уровень, необходимо решить, а что же мы будем тестировать на уровне блоков. Обычно программисты задают несколько случайных массивов данных и считают, что они провели тестирование. Но это можно сделать намного лучше,
Тестирование в рамках контракта
Мы рассматриваем модульное тестирование, как тестирование исходя из контракта (см. "Проектирование по контракту"). Нам бы хотелось написать процедуры тестирования, гарантирующие, что данный модуль соблюдает соответствующий контракт. При этом выясняются два момента: отвечает ли программа условиям контракта, и означает ли контракт на самом деле то, что мы о нем думаем. Мы хотим проверить, обладает ли модуль функциональными характеристиками, которые в нем заложены, используя разнообразные тестовые процедуры и граничные условия.
Что это означает на практике? Рассмотрим подпрограмму извлечения квадратного корня, с которой мы впервые встретились в разделе "ППК и аварийное завершение работы программы". Ее контракт довольно прост:
require:
argument >=0
ensure:
abs((result*result)–argument) < epsilon
Он указывает на моменты, нуждающиеся в проверке:
• Передать отрицательный аргумент и удостовериться в том, что он отклонен
• Передать аргумент, равный нулю, и удостовериться в том, что он принят (это граничное значение)
• Передать значение в интервале от нуля до максимально выражаемого параметра и проверить, что разность между квадратом результата и исходным аргументом меньше некоторой величины "epsilon"
Вооружась этим контрактом и полагая, что наша программа осуществляет собственную проверку предусловий и постусловий, можно записать базовый тестовый сценарий для проверки функции извлечения квадратного корня.
public void testValue(double num, double expected) {
double result = 0.0;
try { // We may throw a
result = mySqrt(num); // precondition exception
}
catch (Throwable e) {
if (num<0.0) // If input is «0, then
Return; // we're expecting the
Else // exception, otherwise
Assert(false); // force a test failure
}
assert(Math.abs(expected-result)<epsilon);
}
Затем мы можем вызвать эту подпрограмму, чтобы проверить нашу функцию излечения квадратного корня:
TestValue(-4.0, 0.0);
TestValue(0.0, 0.0);
TestValue(2.0, 1.4142135624);
TestValue(64.0, 8.0);
TestValue(1.0e7, 3162.2776602);
Это весьма простая процедура тестирования; в реальном мире любой нетривиальный модуль скорее всего будет зависеть от ряда других модулей, поэтому, может быть, есть смысл протестировать их сочетание?
Предположим, есть модуль А, использующий модули LinkedList и Sort. Мы осуществляем тестирование в следующем порядке:
1. Полностью тестируем контракт модуля LinkedList.
2. Полностью тестируем контракт модуля Sort.
3. Тестируем контракт модуля А, который полагается на другие контракты, но не раскрывает их напрямую.
При этом способе тестирования вы вначале обязаны проводить тестирование подкомпонентов.
Если модули LinkedList и Sort успешно прошли тестирование, а модуль А испытания не прошел, мы можем быть вполне уверены, что проблема заключается в модуле А или в том, как модуль А использует один из подкомпонентов. Эта методика способствует уменьшению трудоемкости процесса отладки: можно быстро сосредоточиться на вероятном источнике проблем в пределах модуля А и не тратить время на изучение его подкомпонентов.
Зачем вся эта головная боль? Прежде всего, хотелось бы избежать создания "бомбы замедленного действия", той, что остается незамеченной и позже взрывается в самый неподходящий момент во время работы над проектом. Подчеркивая важность "тестирования в рамках контракта", мы пытаемся, насколько это возможно, избежать катастроф, возникающих в будущем.
Подсказка 48: Проектируйте с учетом тестирования
Когда вы проектируете модуль или даже целую программу, вы обязаны проектировать ее контракт и программу для проверки этого контракта. Проектируя программу, которая проходит тестирование и выполняет соответствующий контракт, вы можете учесть граничные условия и другие аспекты, на которые в иных случаях не обратили бы внимания. Лучше всего устранять ошибки, избежав их с самого начала. На самом деле, при создании процедуры тестирования до реализации программы вам приходится испытывать интерфейс перед тем как принять его.
Создание модульных тестов
Модульные тесты не должны оказываться где-то на периферии исходной древовидной схемы. Они должны располагаться так, чтобы с ними было удобно обращаться. В случае небольших проектов можно внедрить модульный тест в сам модуль. Для более крупных проектов можно поместить каждую из процедур тестирования в отдельный подкаталог. В любом случае необходимо помнить, что если модуль сложно отыскать, то он не будет использован.
Делая тестовую процедуру доступной, вы наделяете разработчиков, которые могут воспользоваться вашей программой, двумя бесценными ресурсами:
1. Примерами того, как использовать все функциональные возможности вашего модуля
2. Средствами построения процедур регрессионного тестирования для проверки правильности любых изменений, которые будут вноситься в программу впоследствии
Если каждый класс или модуль содержит свой собственный модульный тест, это удобно, но не всегда практично. Например, в языке Java каждый класс содержит собственную подпрограмму main. За исключением файла основного класса приложения, подпрограмма main может использоваться для запуска модульных тестов; она будет игнорироваться во время работы самого приложения. Преимущество состоит в том, что программа, отправляемая заказчику, все еще содержит тесты, которые могут использоваться для диагностики проблем, возникающих "в боевой обстановке".