Как тестировщики мы всегда ищем непредвиденные варианты. Пожалуйста, помните, что просто устранить проблему недостаточно. Она может возникнуть снова, когда мы ее не ожидаем. Мы хотим писать тесты, которые систематически выявляют ошибки. Если мы просто исправим этот простой код, то можем либо неправильно решить задачу, либо внести новую ошибку. Цель анализа кода заключается не только в выявлении ошибок (хотя это всегда полезно), а в разработке удобного набора тестов, позволяющих выявить все ошибки (или, говоря более реалистично, большинство ошибок).
Подчеркнем, что циклы всегда содержат неявные инструкции if: они выполняют проверку условия выхода из цикла. Следовательно, циклы также являются инструкциями ветвления. Когда мы анализируем программы, содержащие инструкции ветвления, первым возникает следующий вопрос: все ли ветви мы проверили? Удивительно, но в реальной программе это не всегда возможно (потому что в реальном коде функции вызываются так, как удобно другим функциям, и не всегда любыми способами). Затем возникает следующий вопрос: какую часть кода мы проверили? И в лучшем случае мы можем ответить: “Мы проверили большинство ветвей”, объясняя, почему мы не смогли проверить остальные ветви. В идеале при тестировании мы должны проверить 100% кода.
26.3.4. Системные тесты
Тестирование любой более или менее значимой системы требует опыта. Например, тестирование компьютеров, управляющих телефонной системой, проводится в специально оборудованных комнатах с полками, заполненными компьютерами, имитирующими трафик десятков тысяч людей. Такие системы стоят миллионы долларов и являются результатом работы коллективов очень опытных инженеров. Предполагается, что после их развертывания основные телефонные коммутаторы будут непрерывно работать двадцать лет, а общее время их простоя составит не более двадцати минут (по любым причинам, включая исчезновение энергопитания, наводнения и землетрясения). Мы не будем углубляться в детали — легче научить новичка, не знающего физики, вычислить поправки к курсу космического аппарата, спускающегося на поверхность Марса, — но попытаемся изложить идеи, которые могут оказаться полезными при тестировании менее крупных проектов или для понимания принципов тестирования более крупных систем.
Прежде всего следует вспомнить, что целью тестирования является поиск ошибок, особенно часто встречающихся и потенциально опасных. Написать и выполнить большое количество тестов не просто. Отсюда следует, что для тестировщика крайне желательно понимать сущность тестируемой системы. Для эффективного тестирования систем знание прикладной области еще важнее, чем для тестирования отдельных модулей. Для разработки системы необходимо знать не только язык программирования и компьютерные науки, но и прикладную область, а также людей, которые будут использовать приложение. Это является одной из мотиваций для работы с программами: вы увидите много интересных приложений и встретите
много интересных людей.
Для того чтобы протестировать полную систему, необходимо создать все ее составные части (модули). Это может занять значительное время, поэтому многие системные тесты выполняются только один раз в сутки (часто ночью, когда предполагается, что разработчики спят) после тестирования всех модулей по отдельности. В этом процессе ключевую роль играют регрессивные тесты. Самой подозрительной частью программы, в которой вероятнее всего кроются ошибки, является новый код и те области кода, в которых ранее уже обнаруживались ошибки. По этой причине важной частью тестирования (на основе регрессивных тестов) является выполнение набора предыдущих тестов; без этого крупная система никогда не станет устойчивой. Мы можем вносить новые ошибки с той же скоростью, с которой удаляются старые.
Обратите внимание на то, что мы считаем неизбежным случайное внесение новых ошибок при исправлении старых. Мы рассчитываем, что новых ошибок меньше, чем старых, которые уже удалены, причем последствия новых ошибок менее серьезные. Однако, по крайней мере пока, мы вынуждены повторять регрессивные тесты и добавлять новые тесты для нового кода, предполагая, что наша система вышла из строя (из-за новых ошибок, внесенных в ходе исправления).
26.3.4.1. Зависимости
Представьте себе, что вы сидите перед экраном, стараясь систематически тестировать программу со сложным графическим пользовательским интерфейсом. Где щелкнуть мышью? В каком порядке? Какие значения я должен ввести? В каком порядке? Для любой сложной программы ответить на все эти вопросы практически невозможно. Существует так много возможностей, что стоило бы рассмотреть предложение использовать стаю голубей, которые клевали бы по экрану в случайном порядке (они работали бы всего лишь за птичий корм!). Нанять большое количество новичков и глядеть, как они “клюют”, — довольно распространенная практика, но ее нельзя назвать систематической стратегией. Любое реальное приложение сопровождается неким повторяющимся набором тестов. Как правило, они связаны с проектированием интерфейса, который заменяет графический пользовательский интерфейс.
Зачем человеку сидеть перед экраном с графическим интерфейсом и “клевать”? Причина заключается в том, что тестировщики не могут предвидеть возможные действия пользователя, которые он может предпринять по ошибке, из-за неаккуратности, по наивности, злонамеренно или в спешке. Даже при самом лучшем и самом систематическом тестировании всегда существует необходимость, чтобы систему испытывали живые люди. Опыт показывает, что реальные пользователи любой значительной системы совершают действия, которые не предвидели даже опытные проектировщики, конструкторы и тестировщики. Как гласит программистская пословица: “Как только ты создашь систему, защищенную от дурака, природа создаст еще большего дурака”.
Итак, для тестирования было бы идеальным, если бы графический пользовательский интерфейс просто состоял из обращений к точно определенному интерфейсу главной программы. Иначе говоря, графический пользовательский интерфейс просто предоставляет возможности ввода-вывода, а любая важная обработка данных выполняется отдельно от ввода-вывода. Для этого можно создать другой (неграфический) интерфейс.
Это позволяет писать или генерировать сценарии для главной программы так, как мы это делали при тестировании отдельных модулей (см. раздел 26.3.2). Затем мы можем протестировать главную программу отдельно от графического пользовательского интерфейса.