Словарик философствующего информатика/Тестирование

По материалам книги «Программист-прагматик» Эндрю Ханта и Дэвида Томаса.

БОЛЬШИНСТВО разработчиков ненавидят тестирование. Они стремятся тестировать осторожно, подсознательно ощущая, в каком месте программа может сбоить, и избегая слабых мест. Но прагматики ведут себя по-другому. Мы обладаем мотивацией к отысканию дефектов именно сейчас, чтобы нам не пришлось испытывать позор, когда кто-то другой найдет наши ошибки позже. Поиск дефектов можно уподобить ловле рыбы с помощью сети. Мы используем мелкие, небольшие сети (модульные тесты) для ловли пескарей и большие, крупные сети (комплексные тесты) для ловли акул-убийц. Иногда рыбе удается выскользнуть, поэтому мы заделываем все найденные дыры в надежде поймать как можно больше скользких дефектов, плавающих в бассейне нашего проекта.

Тестируйте рано. Часто. Автоматически.

Как только у нас появляется текст программы, мы сразу хотим начать его тестирование. Крошечные пескарики имеют отвратительную привычку быстро становиться огромными акулами-людоедами, а поймать акулу намного сложнее. Но мы не хотим осуществлять все это тестирование вручную. Многие команды разрабатывают сложные планы тестирования своих проектов. Иногда они даже их используют. Но мы обнаружили, что команды, использующие автоматизированные процедуры тестирования, имеют больше шансов на успех. Тесты, запускающиеся в ходе каждого процесса сборки, являются более эффективными по сравнению с планами тестирования, которые лежат на полке. Чем раньше обнаружен дефект, тем дешевле обходится его устранение. «Чуть-чуть напишешь, чуть-чуть проверишь» — популярное изречение в мире Smalltalk, и мы можем принять эту мантру как нашу личную, создавая одновременно (или даже раньше) с написанием рабочей программы программу ее тестирования. На самом деле удачный проект содержит больше программ тестирования, чем рабочих программ. Временные затраты на написание тестовой программы себя оправдывают. В конечном счете это оказывается намного дешевле, и вы действительно имеете возможность создания практически бездефектного продукта. Кроме того, осознание, что вы прошли тест, дает вам большую степень уверенности в том, что этот фрагмент программы готов.

Программа не считается написанной, пока не пройдет тестирование.

На сайте extreme Programming эта концепция обозначена как "непрерывная интеграция, безжалостное тестирование". Тот факт, что вы закончили работу с фрагментом программы, вовсе не означает, что можно идти к шефу или заказчику, чтобы сообщить ему о "готовности". Фрагмент не готов. Прежде всего, программа в реальности никогда не бывает готовой. И, что более важно, пока она не пройдет все имеющиеся тесты, вы не можете утверждать, что она может использоваться кем бы то ни было. Следует рассмотреть три основных аспекта тестирования в масштабе всего проекта: что тестировать, как тестировать и когда тестировать.

Что тестировать

править

Существует несколько видов процедур тестирования программного обеспечения, которые вам приходится выполнять:

  • Модульное тестирование
  • Комплексное тестирование
  • Подтверждение правильности и верификация
  • Тестирование в условиях нехватки ресурсов, ошибки и их исправление
  • Тестирование производительности
  • Тестирование удобства использования

Этот перечень ни в коей мере не является полным, и в некоторых специализированных проектах потребуются другие виды процедур тестирования. Но это дает нам хорошую отправную точку.

Модульное тестирование

править

Модульный тест представляет собой программу, занимающуюся тестированием некоего модуля. Эта тема освещена в разделе "Программа, которую легко тестировать". Модульное тестирование является основой для всех других видов тестирования, которые обсуждаются в данном разделе. Если части не работают по отдельности, то скорее всего они не будут хорошо работать и вместе. Все используемые модули обязаны пройти собственное модульное тестирование перед тем как продолжать работу. Как только все соответствующие модули прошли индивидуальное тестирование, вы готовы к новому этапу. Вам придется проверить, как модули используют друг друга и взаимодействуют между собой по всей системе.

Комплексное тестирование

править

Комплексное тестирование показывает, что основные подсистемы, из которых состоит проект, работают и нормально взаимодействуют друг с другом. При наличии удачных и хорошо проверенных контрактов обнаружить любые проблемы интеграции не составляет особого труда. В противном случае интеграция становится благодатной почвой для размножения дефектов. Фактически в многих случаях она является единственным и самым крупным источником дефектов в системе. В реальности комплексное тестирование является продолжением модульного тестирования, описанного выше, с той лишь разницей, что теперь вы проверяете, как целые подсистемы соблюдают свои контракты.

Подтверждение правильности и верификация

править

Как только в вашем распоряжении появляется рабочий пользовательский интерфейс или прототип, вам приходится отвечать на существенный вопрос: пользователи сказали вам, что они хотели бы увидеть, но то ли это на самом деле? Отвечает ли продукт функциональным требованиям системы? Это также нуждается в тестировании. Бездефектная система, которая отвечает на неправильные вопросы, не приносит пользы. Надо осознавать схемы доступа конечного пользователя и их отличие от тестовых данных разработчика (в качестве примера обратите внимание на историю о рисовании кистью из раздела "Отладка").

Тестирование в условиях нехватки ресурсов, ошибки и их исправление

править

Теперь вы понимаете, что система будет вести себя корректно в идеальных условиях, вам придется испытать, как она ведет себя в реальных условиях. В реальном мире ресурсы ваших программ не безграничны — они исчерпываются. Ваша программа может столкнуться со следующими ограничениями:

  • Объем памяти
  • Объем дискового пространства
  • Мощность процессора
  • Тактовая частота
  • Скорость дискового обмена
  • Пропускная способность сети
  • Цветовая палитра
  • Разрешающая способность экрана

Вы можете реально проверить нехватку дискового пространства или объема памяти, но как часто вы проверяете другие ограничения? Будет ли ваше приложение работать на экране с разрешением 640 х 480 и 256 цветами? Может ли оно выполняться на экране с разрешением 1600 х 1280 с 24-битным цветом и при этом не быть размером с почтовую марку? Завершится ли пакетное задание до момента запуска программы архивации? Вы можете обнаружить наличие ограничений в операционной среде, таких как спецификация видеоподсистемы, и приспособиться к ним соответствующим образом. Однако не все сбои можно восстановить. Если программа обнаруживает нехватку памяти, то вы ограничены в своих действиях: вам не хватит ресурсов, чтобы завершить программу способом, отличным от аварийного завершения. Когда система выходит из строя, будет ли это делаться изящно? Постарается ли она сделать лучшее, на что она способна в данной ситуации, — сохранить? Редактор американского издания требовал изменить это предложение на "Если система выходит из строя ... ". Авторы сопротивлялись.

Тестирование производительности

править

Тестирование производительности, нагрузочное тестирование или тестирование в реальных условиях эксплуатации может также оказаться важным аспектом проекта. Задайте себе вопрос, отвечает ли программа требованиям производительности в условиях реального мира — с ожидаемым числом пользователей, подключений или транзакций в единицу времени. Является ли она масштабируемой? При работе с некоторыми приложениями вам могут понадобиться специализированные тестовая аппаратура или программное обеспечение для реалистичной имитации нагрузки.

Тестирование удобства использования

править

Тестирование удобства использования отличается от процедур тестирования, обсужденных выше. Оно осуществляется с реальными пользователями в реальных условиях окружающей среды. Рассмотрим удобство использования с точки зрения человеческого фактора. Были ли важные недоразумения в ходе анализа требований? Подходит ли программное обеспечение пользователю, становясь продолжением его рук? {Мы хотим, чтобы не только наши собственные инструменты были изготовлены по руке, но чтобы и те, которые мы создаем для пользователей, подходили им.) Как и при подтверждении правильности и верификации, вам приходится осуществлять тестирование удобства использования как можно раньше, пока есть время на внесение изменений. Для крупномасштабных проектов вы можете привлечь специалистов в области человеческого фактора. Несоответствие критериям удобства использования является дефектом такого же порядка, как деление на ноль.

Как проводить тестирование

править

Мы рассмотрели то, что подлежит тестированию. Теперь мы обратим внимание на то, как это делается, включая следующее:

  • Регрессионное тестирование
  • Тестовые данные
  • Тестирование систем с графическим интерфейсом
  • Тестирование самих тестов
  • Исчерпывающее тестирование

Тестирование проектных решений/методологии

править

Можете ли вы провести тестирование проектных решений в самой программе и методологии, которую вы использовали при сборке программного обеспечения? Некоторым образом можете. Вы делаете это, анализируя метрики — измерения различных аспектов программы. Самой простой метрикой (и чаще всего, наименее интересной) является число строк кода — насколько велика сама программа? Существует большое количество других метрик, которые вы можете использовать для исследования программы:

  • Показатель цикломатической сложности Маккейба (измеряет сложность структуры решений)
  • Коэффициент разветвления по входу при наследовании (количество базовых классов) и по выходу (количество производных модулей; используется в качестве родителя)
  • Набор откликов (см. раздел "Несвязанность и закон Деметера")
  • Отношения связывания класса

Некоторые метрики предназначены для того, чтобы дать вам "проходной балл", тогда как другие полезны только в сравнении. Это означает, что вы вычисляете метрики для каждого модуля в системе и смотрите, как конкретный модуль относится к своим братьям. Здесь обычно используются стандартные статистические методики. Если вы обнаруживаете модуль, чья метрика значительно отличается от всех остальных, вам следует задать вопрос, приемлемо ли это. Для некоторых модулей "нарушение хода кривой" может быть вполне нормально. Но для тех, у которых нет хорошего оправдания, это может свидетельствовать о потенциальных проблемах.

Регрессионное тестирование

править

Регрессионное тестирование сравнивает выходные данные текущего теста с результатами (или известными значениями} предыдущих. Мы можем гарантировать, что дефекты, устраненные сегодня, не нарушат ничего из того, над чем мы работали вчера. Это важное средство страховки, и оно сокращает число неприятных сюрпризов. Все тесты, о которых мы говорили до настоящего момента, могут запускаться как регрессионные тесты с гарантией, что мы не откатываемся назад, когда разрабатываем новую программу. Мы можем запускать регрессии для тестирования производительности, контрактов, достоверности и т.д.

Тестовые дaнные

править

Где мы достаем данные для запуска всех этих тестов? Существует только два типа данных: реальные и синтезированные данные. В действительности нам надо использовать оба типа, поскольку их различная природа будет способствовать выявлению разных дефектов в программном обеспечении. Реальные данные исходят из некоего реального источника. Возможно, они были получены из существующей системы, конкурирующей системы или некоего прототипа. Они представляют типичные пользовательские данные. Большие сюрпризы возникают, как только вы открываете значение термина "типичный". При этом скорее всего являются дефекты и недоразумения в анализе требований. Синтезированные данные генерируются искусственно, возможно, с определенными статистическими ограничениями. Вам могут понадобиться синтезированные данные по одной из следующих причин:

  • Вам нужно много данных, возможно, больше, чем содержится в любом из имеющихся образцов. Вы сможете использовать реальные данные в качестве "саженца" для генерации большего набора данных и добиться уникальности определенных полей.
  • Вам требуются данные для того, чтобы выделить определенные граничные условия. Эти данные могут быть полностью синтезированными: поля, содержащие дату 29 февраля 1999 г., огромные размеры записей или адреса с иностранными почтовыми индексами.
  • Вам требуются данные, которые демонстрируют определенные статистические свойства. Вы хотите увидеть, что случается, если сбой происходит с каждой третьей транзакцией? Вспомните алгоритм сортировки, который замедляется и ползет, когда обрабатывает предварительно отсортированные данные. Чтобы продемонстрировать эту слабость, вы можете представить данные в случайном или упорядоченном виде.

Тестирование систем с графическим интерфейсом

править

Тестирование систем, насыщенных графическими интерфейсами, часто требует наличия специализированных инструментальных средств. Эти средства могут основываться на простой модели захвата/воспроизведения данных или могут потребовать специально для этой цели написанных сценариев для управления графическим интерфейсом. Некоторые системы объединяют элементы обеих моделей. Менее сложные инструментальные средства предписывают высокую степень связывания тестируемой версии программы и самого тестового сценария: если вы перемещаете диалоговое окно или уменьшаете размер экранной кнопки, процедура тестирования может не найти всего этого и оказаться неудачной. Большинство современных инструментальных средств тестирования графических интерфейсов используют ряд методик, чтобы обойти эту проблему и попытаться приспособиться к незначительным различиям в компоновке. Однако вы не можете автоматизировать всё. Энди работал над графической системой, которая позволяла пользователю создавать и отображать недетерминированные визуальные эффекты, моделирующие различные природные явления. К сожалению, в ходе тестирования нельзя просто захватить растровое изображение и сравнить с предыдущим прогоном, потому что приложение было спроектировано так, что каждый раз оно выполнялось по-разному. В подобных ситуациях у вас может не быть выбора, кроме как положиться на ручную интерпретацию результатов теста. Одним из преимуществ, возникающих при написании несвязанной программы (см. "Несвязанность и закон Деметера"} является большая доля модульного тестирования. Например, для приложений, занимающихся обработкой данных, которые имеют внешний графический интерфейс, конструкция должна быть несвязанной в достаточной степени, чтобы можно было тестировать логику приложения в отсутствии графического интерфейса. Эта идея аналогична необходимости тестировать компоненты в числе первых. Как только достоверность логики приложения подтверждается, задача по поиску дефектов, которые выявляются при наличии пользовательского интерфейса, не представляет труда (скорее всего, эти дефекты были созданы программой интерфейса пользователя).

Тестирование самих тестов

править

Поскольку мы не можем писать совершенные программы, то из этого следует, что мы не можем написать и совершенные программы для тестирования. Нам следует тестировать сами тесты. Рассматривайте набор тестовых пакетов как сложную систему безопасности, предназначенную для подачи звукового сигнала тревоги при выявлении дефекта. Ведь нет лучшего способа проверки безопасности системы, как попытаться вломиться в нее? После того как вы написали тест для обнаружения конкретного дефекта, вызовите этот дефект преднамеренно и удостоверьтесь, что тест его обнаружил. Это гарантия того, что тест обязательно выловит этот дефект в реальных условиях.

Используйте диверсантов для тестирования самих тестов

Если вы серьезно относитесь к тестированию, то вы должны нанять диверсанта проекта, чья роль состоит в том, чтобы воспользоваться отдельной копией исходного дерева, преднамеренно внести дефекты и проверить, что при тестировании они будут выловлены. При написании тестов убедитесь, что сигналы тревоги раздаются тогда, когда они обязаны раздаваться.

Исчерпывающее тестирование

править

Вы уверены в том, что ваши тесты являются корректными и обнаруживают созданные вами дефекты. Но как вы узнаете о том, насколько исчерпывающе вы провели тестирование ядра программы? Ответ здесь краток: "никак", вы никогда это не узнаете. Но на программном рынке есть продукты, которые могут вам помочь. Эти средства анализа степени покрытия отслеживают программу при тестировании и регистрируют, какие строки были выполнены, а какие нет. Эти средства дают общее представление о том, насколько исчерпывающей является процедура тестирования, но не стоит ожидать, что степень покрытия составит 100%. Даже если выполненными окажутся все строки программы, это еще не все. Важно то число состояний, в которых может находиться программа. Состояния не являются эквивалентом строк программы. Предположим, что есть функция, обрабатывающая два целых числа, каждое из которых может принимать значения от 0 до 999.

int test(int a, int b) { return a / (a + b) }

Теоретически эта функция, состоящая из трех строк, имеет 1000000 логических состояний, 999999 из которых будут работать правильно, а одно — неправильно (когда а + b равно нулю). Если вам известно лишь то, что данная строка программы выполнена, то вам придется идентифицировать все возможные состояния программы. К сожалению, это очень сложная проблема. Настолько сложная, что "пока вы ее решите, солнце превратится в холодную глыбу".

Тестируйте степень покрытия состояний, а не строк текста программы

Даже при высокой степени покрытия программы данные, используемые вами в процедуре тестирования, все еще оказывают огромное влияние, и, что более важно, порядок, в котором вы выполняете программу, может оказать самое сильное воздействие.

Когда тестировать

править

Многие проекты стремятся отложить процедуру тестирование на последний момент — тот, где оно будет срезано в преддверии контрольного срока. Нужно начать тестирование намного раньше наступления этого срока. Как только появится какая-либо рабочая программа, ее необходимо протестировать. Большинство процедур тестирования должно выполняться автоматически. Важно заметить, что под термином "автоматически" мы имеем в виду и автоматическую интерпретацию результатов теста. Более подробно этот аспект рассматривается в разделе "Вездесущая автоматизация". Мы предпочитаем проводить тестирование как можно чаще и всегда делаем это перед возвращением исходного текста в библиотеку. Некоторые системы управления исходным текстом, наподобие Aegis, могут осуществлять это автоматически. В других случаях мы просто набираем

 % make test

Обычно не представляет труда запускать регрессии на всех отдельных модульных и комплексных тестах и проделывать это так часто, как оно требуется. Но для ряда тестов частый прогон может представлять сложность. Для проведения нагрузочного тестирования могут потребоваться специальные настройки или оборудование и некоторая часть ручной работы. Эти тесты могут проводиться с меньшей частотой — возможно, еженедельно или ежемесячно. Но важно то, что они прогоняются на регулярной, запланированной основе. Если это нельзя сделать автоматически, то удостоверьтесь, что тесты включены в план вместе со всеми ресурсами, назначенными для данной задачи.

Кольцо сжимается

править

И наконец, мы хотели бы раскрыть единственный и самый важный принцип тестирования. Он очевиден, и практически в каждом учебнике говорится о том, что это нужно делать именно так. Но в силу некоторых причин в большинстве проектов этого все еще не делается. Если дефект проскальзывает через сеть существующих тестов, вам следует добавить новый тест, чтобы поймать его в следующий раз.

Дефект должен обнаруживаться единожды

Если тестировщик обнаруживает дефект, это должно быть в первый и последний раз — обнаружение дефекта человеком. Автоматизированные тесты должны быть модифицированы для проверки наличия этого дефекта, начиная с момента его первоначального обнаружения, всякий раз, без каких-либо исключений, не обращая внимания на степень тривиальности, жалобы разработчика и его фразу "Этого больше не случится". Потому что это снова случится. А у нас просто нет времени гоняться за дефектами, которые автоматизированные тесты не могли обнаружить. И нам придется тратить время на написание новой программы — с новыми дефектами.