Для каждого языка существуют свои специализированные средства модульного тестирования. Лично я использую JUnit для Java, rspec для Ruby, NUnit для. Net, Midje для Clojure и CppUTest для C и C++.
Впрочем, какой бы инструмент модульного тестирования вы ни выбрали, все они должны обладать набором базовых свойств и функций.
1. Тесты должны запускаться легко и быстро. Не важно, как именно это делается – при помощи плагинов IDE или простых утилит командной строки; главное, чтобы разработчики могли запускать эти тесты по своему усмотрению. Команда запуска должна быть тривиально простой. Например, я запускаю свои тесты CppUTest в TextMate простым нажатием клавиш command+M. Я связал эту комбинацию с запуском makefile, который автоматически выполняет тесты и выводит короткий однострочный отчет в том случае, если все тесты прошли успешно. IntelliJ поддерживает JUnit и rspec, поэтому от меня требуется лишь нажать кнопку. Для выполнения NUnit я использую плагин Resharper.
2. Программа должна выдавать четкий визуальный признак прохождения/непрохождения теста. Не важно, будет ли это зеленая полоса на графическом экране или консольное сообщение «Все тесты прошли». Суть в том, чтобы вы могли быстро и однозначно определить, что все тесты прошли. Если для определения результата тестирования приходится читать многострочный отчет или, того хуже, сравнивать выходные данные двух файлов, – условие не выполнено.
3. Программа должна выдавать четкий визуальный признак прогресса. Не важно, будет ли это графический индикатор или строка постепенно появляющихся точек – главное, чтобы пользователь видел, что процесс идет, а тестирование не остановилось и не прервалось.
4. Программа должна препятствовать взаимодействию между отдельными тестовыми сценариями. JUnit решает эту проблему, создавая новый экземпляр тестового класса для каждого тестового метода; таким образом предотвращается использование переменных экземпляров для взаимодействия между тестами. Другие инструменты запускают тестовые методы в случайном порядке, чтобы исключить возможную зависимость от конкретного порядка выполнения тестов. Независимо от конкретного выбора механизма, программа должна принять меры к обеспечению независимости тестов. Зависимые тесты – опасная ловушка, которую необходимо избегать.
5. Программа должна по возможности упрощать написание тестов. Например, JUnit предоставляет удобный API для проверки условий (assertions), а также использует рефлексию и атрибуты Java для того, чтобы отличать тестовые функции от обычных. В результате хорошие IDE могут автоматически идентифицировать тесты, а вы избавляетесь от хлопот с подготовкой тестов.
Инструменты компонентного тестирования
Инструменты этой категории предназначены для тестирования компонентов на уровне API. Их роль – определение поведения компонента на языке, понятном для бизнеса и специалистов по контролю качества. В идеале бизнес-аналитики и специалисты по контролю качества должны писать спецификации с использованием данных инструментов.
Определение
Инструменты компонентного тестирования в большей степени, чем какие-либо другие инструменты, выражают наше представление о том, что понимать под «выполненной» работой. Когда бизнес-аналитики и специалисты по контролю качества создают спецификацию, определяющую поведение компонента, и когда эта спецификация выполняется как набор тестов, которые либо проходят, либо нет, понятие «готово» принимает совершенно однозначный смысл: «Проходят все тесты».
FitNesse
Мой любимый инструмент компонентного тестирования – FitNesse. Я написал большую часть его кода, и я являюсь его основным автором. В общем, это мое детище.
FitNesse – система на базе вики, которая позволяет бизнес-аналитикам и специалистам по контролю качества писать тесты в очень простом табличном формате. Эти таблицы имеют много общего с таблицами Парнаса по форме и предназначению. Тесты легко объединяются в пакеты, которые можно выполнить в любой момент.
Система FitNesse написана на Java, однако она может использоваться для тестирования систем на любых языках, потому что она взаимодействует с базовой системой тестирования, которая может быть написана на любом языке. В настоящее время поддерживаются языки Java, C#/.NET, C, C++, Python, Ruby, PHP, Delphi и другие.
В основе FitNesse лежат две системы: Fit и Slim. Система Fit была написана Уордом Каннингемом, послужила источником вдохновения для FitNesse и является ее близким родственником. Slim – более простая и лучше портируемая система тестирования, которой сейчас пользуются многие сторонники FitNesse.
Другие инструменты
Я знаю еще несколько других инструментов, которые можно отнести к категории компонентного тестирования.
• Система RobotFX была разработана инженерами Nokia. Как и в FitNesse, в ней используется табличный формат, но не на базе вики. RobotFX просто работает с неструктурированными файлами, созданными в Excel или другой аналогичной программе. Программа написана на Python, но при использовании соответствующих «мостов» может тестировать системы, написанные на любом языке.
• Green Pepper – коммерческая программа, в некоторых аспектах похожая на FitNesse. Базируется на популярной вики confluence.
• Cucumber – текстовая программа, работающая на ядре Ruby, но подходящая для тестирования на многих разных платформах. В языке Cucumber используется популярный стиль Given/When/Then.
• JBehave – аналог и «идеологический родитель» Cucumber. Программа написана на Java.
Инструменты интеграционного тестирования
Инструменты компонентного тестирования также могут использоваться для выполнения многих интеграционных тестов, но они плохо подходят для тестов, управляемых через пользовательский интерфейс.
В общем случае количество тестов, управляемых через пользовательский интерфейс, должно быть минимальным, потому что пользовательские интерфейсы хорошо известны своей нестабильностью. Из-за этой нестабильности тесты, выполняемые через пользовательский интерфейс, становятся крайне непрочными.
Впрочем, некоторые тесты должны выполняться через пользовательский интерфейс – прежде всего это тесты самого интерфейса. Кроме того, некоторые комплексные тесты должны проходить через всю собранную систему, включая пользовательский интерфейс.
Для тестирования пользовательского интерфейса я предпочитаю использовать программы Selenium и Watir.
UML/MDA
В начале 1990-х годов я очень надеялся, что отрасль автоматизированной разработки ПО кардинально изменит работу программиста. Заглядывая в будущее из этих бурных дней, я думал, что к сегодняшнему дню все уже будут программировать с диаграммами на более высоком уровне абстракции, а текстовый код останется в прошлом.
Как же я ошибался! Мои мечты не только не сбылись, но и любые попытки движения в этом направлении потерпели полный крах. Дело даже не в том, что не существует инструментов и систем, демонстрирующих потенциал такого подхода; просто эти инструменты толком не реализуют эту мечту и ими никто не хочет пользоваться.
Я мечтал, что разработчики избавятся от подробностей текстового кода и начнут строить системы на более высоком диаграммном уровне. В самых дерзких мечтах программисты вообще становились лишними. Архитекторы могли строить целые системы по диаграммам UML. Ядро системы – огромное, бесстрастное и равнодушное к бедам обычных программистов – преобразует диаграммы в исполняемый код. Так выглядела великая мечта модельно-ориентированной архитектуры (MDA, Model Driven Architecture).
К сожалению, у великой мечты имеется один крошечный изъян. MDA предполагает, что проблемы создает код. Однако код сам по себе не создает проблем и никогда не создавал. Проблема заключается в детализации.
Детализация
Программисты управляют подробностями. Да, именно этим мы и занимаемся. Мы описываем поведение системы в мельчайших подробностях. Для этой цели мы используем текстовые языки, потому что они чрезвычайно удобны (для примера возьмите хотя бы английский).
Какими же подробностями мы управляем?
Вы знаете, чем различаются два символа n и r? Первый, n – перевод строки (line feed), а второй, r – возврат каретки (carriage return). Что такое «каретка»?
В 1960-х и начале 1970-х годов основным устройством вывода для компьютеров был телетайп. Модель ASR33[55] была самой распространенной.
Устройство состояло из печатающей головки, которая могла печатать до 10 символов в секунду. Печатающая головка состояла из цилиндра, на котором были нанесены рельефные символы. Цилиндр поворачивался так, чтобы к бумаге был обращен правильный символ, после чего молоточек ударял по цилиндру и прижимал его к бумаге. Между цилиндром и бумагой проходила красящая лента, краска с которой переносилась на бумагу в виде символа.