« "Серебряная пуля или горькая пилюля?", Кен Швабер | | "Варианты использования, десять лет спустя", Алистэр Коуберн »

"Организация и именование автоматизированных тестов", Кирилл Максимов

Кирилл Максимов <kir@maxkir.com>
февраль-март, 2003

Введение

На текущий момент уже написано множество статей и книг на тему автоматизированного тестирования программного кода. При этом очень немногие уделяют внимание вопросу организации и наименования таких тестов - по крайней мере, я не нашел каких-либо руководств на эту тему. А между тем, при существенном увеличении количества тестов бывает сложно понять, куда поместить тот или иной автоматизированный тест и как его назвать. Это стало особенно актуально с появлением концепции Test-Driven Development (TDD).

Примерный смысловой перевод Test-Driven Development на русский язык звучит как "разработка через тестирование". Эта концепция сравнительно недавно отпочковалась от методологии Экстремальное программирование (eXtreme Programming, XP). Ее основные положения следующие:
  • Перед написанием любого фрагмента кода разрабатываемой системы пишется автоматизированный тест, который проверяет функционирование этого фрагмента кода (естественно, поначалу этот тест не проходит).
  • После того, как написанный тест стал проходить, необходимо устранить дублирование в коде.
Такой подход может использоваться любым программистом и не подразумевает использование какой-либо методологии.

В данной статье мне хотелось бы остановиться именно на вопросе организации автоматизированных тестов. Безусловно, эта тема весьма обширна и заслуживает написания отдельной научной работы (или хотя бы книжки на несколько сотен страниц). Но я буду краток и просто обрисую те приемы, которые помогали мне в работе, и которые считаю наиболее удачными. В своем изложении я буду ориентироваться на язык Java и библиотеку для тестирования JUnit, хотя многое из нижеизложенного может быть применено и к другим библиотекам для написания автоматизированных тестов.

Для начала определим названия для тестов.

Типы тестов

Тип тестов Описание
Тесты модулей
(Unit tests)
Эти тесты служат для проверки правильности работы отдельных модулей системы, как правило - классов. При этом внешние по отношению к тестируемому классу ресурсы, такие как БД или другие модули тестируемого приложения, подменяются mock-объектами. Конечно, такая замена производится не для всех задействованных классов, а только для тех, которые сложно или долго воссоздать в тестовом окружении.
Тесты заказчика Под этим названием я хочу объединить сразу несколько типов тестов: функциональные, системные и приемочные тесты. Все они проверяют поведение системы в целом. В методологии XP эти тесты пишет заказчик, используя предоставленный программный каркас. В менее радикальных методологиях за написание этих тестов отвечает персонал отдела контроля качества (QA), который создает эти тесты, основываясь на требованиях к системе, согласованных с тем же заказчиком.

В качестве примера каркасов тестирования Web-приложений стоит упомянуть HtmlUnit, XMLUnit, jWebUnit.

Интеграционные тесты
(Integration tests)
Интеграционные тесты представляют собой нечто среднее между тестами заказчика и тестами модулей. В отличие от последних, интеграционные тесты служат для тестирования взаимодействия нескольких уровней приложения. При написании этих тестов mock-объекты практически не используются, что ведет за собой увеличение времени тестирования. Кроме того, интеграционные тесты требуют наличия специального тестового окружения (например, в виде заранее подготовленных данных в БД) или использования специальных библиотек. Примером такой библиотеки для интеграционного тестирования J2EE приложений может служить Cactus, а для подготовки тестовых данных может использоваться DBUnit.

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

Организация тестов, которые пишут разработчики (Developer tests)

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

Далее в этом разделе под термином "тест" я буду иметь в виду автоматизированный модульный или интеграционный тест.

Проблемы, решаемые правильной организацией тестов

В процессе разработки системы перед программистом часто встает вопрос: а существует ли тест для данного поведения системы и если существует, то где его найти? Классический пример - исправление ошибки в системе (при этом ошибка обнаружена не автоматизированными тестами). Последовательность действий здесь примерно такая:

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

Обращаю ваше внимание на то, что в процессе исправления бага разработчик:

  1. искал существующий тест(ы), связанный с данной функциональностью;
  2. выбрал некоторое имя для нового теста (включая пакет, класс, метод);

Давайте рассмотрим возможные варианты поведения разработчика при решении этих задач.

Поиск существующих тестов для функциональности

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

Выбор имени для нового теста

Эта задача возникает значительно чаще, чем предыдущая. Особенно актуальной она становится при использовании Test Driven Development (TDD), когда тест пишется еще до написания тестируемого кода. Одним из наиболее популярных подходов является формирование имен тестовых классов и методов на основе имен тестируемых классов и методов. То есть, если мы тестируем класс Foo, то тестовый класс называется FooTest. А если тестируем метод bar(), то называем тестовые методы testBarOK() и testBarError(). Все просто, не так ли? Не совсем. И вот почему:
  • При использовании TDD тестируемый класс или метод может вообще еще не существовать.
  • Одна и та же тестовая конфигурация (fixture) может использоваться для тестирования нескольких классов.
  • Один тест может проверять поведение и демонстрировать использование нескольких методов.
  • Возникают проблемы при переименовании класса или метода. Название теста тут же перестает соответствовать действительности и, более того, может ввести в заблуждение. Хотя здесь мог бы помочь хороший refactoring tool.

Опять-таки, наличие продуманной системы организации и именования тестов могло бы значительно упростить решение проблемы выбора имени для теста.

Рекомендации

Ниже я хочу поделиться некоторыми рекомендациями по организации тестов модулей и интеграционных тестов.

Общие замечания

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

Необходимость деления тестов на модульные и интеграционные зависит от сложности системы. Если такое деление существует, то имеет смысл использовать отдельные наборы (suites) тестов для этих двух групп. При этом запуск всех модульных тестов может производиться перед внесением написанного кода в общий репозиторий, а запуск всех интеграционных тестов (и тестов заказчика) - с определенным периодом по времени, в автоматическом режиме. Для реализации такой схемы запуска тестов можно использовать такие программы как CruiseControl или AntHill.

Выбор названия для теста

В качестве общего правила выбора названия для теста, который пишет разработчик, я бы рекомендовал следующее:
Имя тестового класса/метода должно предоставлять информацию о том поведении, которое тестируется этим классом/методом.
Разумеется, это правило не запрещает использовать имена тестируемых классов и методов в именах тестов. Но оно заставляет думать несколько в другом направлении и ориентировать тесты не на абстрактное поведение конкретных методов и классов, а на решение задач, которые стоят перед системой. Как минимум, у вас не будет соблазна написать тест, проверяющий передачу параметра null в какой-либо метод, если в реальном коде системы такого произойти никак не может.

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

Иногда для теста довольно сложно выбрать "говорящее название" (или же оно получается слишком длинным). В таких случаях надо задуматься - а не стоит ли выделить в отдельный класс еще одну специализированную тестовую конфигурацию (fixture) и назвать ее соответствующим образом. В рамках такой конфигурации выбрать подходящее название для теста может оказаться проще. Например, у меня есть класс AccessValidator, который осуществляет проверку прав пользователей на различные объекты. Допустим, у меня есть тестовый класс TestAccess и метод testAccessToStatisticsForManager_OK. Если у меня есть несколько тестовых методов, названия которых оканчиваются на "ForManager", возможно, имеет смысл выделить тестовую конфигурацию TestAccessForManager, и переместить туда эти тестовые медоды, убрав из них окончание "ForManager". Кроме реорганизации имен (которая, кстати, уменьшает дублирование в тестовом коде), это позволит вынести общий код cоздания объекта "Manager" в метод setUp.

Вообще, имя тестового класса должно отражать ту конфигурацию, которая используется в тестах, содержащихся в данном классе. Например, название некоторого класса может выглядеть как TestCreditCardProcessor_ValidCreditCardNumberEntered. То есть, тестируется один или несколько классов, представляющих собой некоторую подсистему работы с кредитными картами в состоянии, когда клиент уже ввел номер кредитной карты и этот номер валиден. Конечно, если тестов для подсистемы работы с кредитными картами не очень много, то можно обойтись одним тестовым классом TestCreditCardProcessor.

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

	testEnoughMoneyOnAccount_TransactionOK
	testNotEnoughMoneyOnAccount_Exception
	testNoMoneyOnAccount_Exception
	testTransactionCanceled
Обратите внимание, что тесты не указывают на имена конкретных тестируемых классов и методов, но, тем не менее, позволяют отыскать необходимый тест.

Комментарии к тестам

Идея описать тестируемый сценарий в комментариях к тестовому методу мне кажется не очень удачной по двум причинам:
  1. Как и любой комментарий, он имеет тенденцию устаревать по мере внесения в тест изменений. Гораздо заметнее, если содержание теста перестает соответствовать его названию, и это скорее подвигнет на исправление ситуации.
  2. Когда я хочу найти какой-либо тест, я просматриваю список всех методов при помощи моей IDE. При этом названия тестов я вижу, а комментарии к ним - нет. Поэтому я предпочитаю иметь говорящие имена для тестовых методов (пусть даже они получаются слегка длинноваты), а комментарии к ним, за редким исключением, не пишу.

Размещение тестов относительно тестируемого кода

Существуют следующие варианты размещения тестов относительно тестируемого кода (для Java):
  1. тот же пакет, тот же каталог
  2. отдельный тестовый пакет
  3. тот же пакет, другой каталог
  4. вложенный класс
Первый вариант предполагает размещение тестов в каталоге, где находится тестируемый код. Это удобно, так как тестируемый и тестирующий код находятся в соседних файлах. Однако часто такое решение оказывается несколько громоздким из-за большого количества файлов в каталоге. Разобраться в этой мешанине бывает весьма непросто. В такой ситуации часто используют стандартный префикс для классов с тестами, такой как "UTest" или "test", чтобы визуально выделить их в общей массе файлов и отделить их от основного кода. Это разделение также может понадобиться для исключения тестовых классов из инсталляционного пакета. Кроме того, специальные имена позволяют сформировать тестовый набор при помощи тэга batchtest цели junit в системе Ant.

Второй вариант предполагает размещение классов с тестами в отдельном пакете (например, это может быть вложенный пакет "test"). В этом случае весьма затруднено тестирование не-открытых методов тестируемых классов. Это очень неудобно и поэтому такой подход я не хочу рекомендовать ни в коем случае.

Существует возможность размещать тесты в том же пакете что и тестируемый код, но при этом использовать другое дерево каталогов (третий вариант):

src/
    aaa/
        /bbb
            /ccc
tests/
    aaa/
        /bbb
            /ccc
Большинство современных IDE умеет работать с несколькими каталогами исходных файлов и правильно обработает такую конфигурацию. Таким образом можно убить двух зайцев: с одной стороны, тестируемый и тестирующий код находятся в одном пакете (из тестов можно получить доступ к методам с пакетной областью видимости), а с другой - физически файлы с тестами отделены от рабочего кода. Такой способ организации тестов, на мой взгляд, является наиболее удачным.

Последний вариант - использование вложенного класса - частично оправдан в случае, если совершенно необходимо протестировать закрытый метод класса. В целом тестирование закрытых методов я бы рекомендовать не стал - при этом тестируется не поведение, а реализация, что зачастую приводит к правке большего количества тестов при внесении изменений в детали реализации. Но даже если вы решили написать тест на закрытый метод, есть альтернативные решения размещению тестового класса в качестве вложенного - использование механизма reflection. Этот подход реализован классом PrivateAccessor в проекте junit-addons.

Организация тестов заказчика

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

Решаемые задачи

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

Правильная организация тестов заказчика помогает решить следующие задачи:

  • Выяснить, существует ли тест, проверяющий работоспособность некоторого сценария работы с системой. Предполагается, что этот сценарий относится к некоторому варианту использования, реализуемому в данной системе.
  • Получить список тестов, касающихся данного варианта использования. Такой список помогает определить, насколько подробно проработан данный вариант использования и сколько альтернативных сценариев было реализовано.
  • Получить полный (и, желательно, структурированный) список реализованных тестов. Такой список может служить некоторым показателем объема проекта и реализованной функциональности.

Рекомендации

Общие замечания

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

  1. Тесты для конкретного варианта использования располагаются в отдельном пакете, имя которого должно представлять собой сокращенное его название. Таким образом, простой просмотр существующих пакетов с тестами позволяет получить список протестированных вариантов использования. При этом желательно отделить тесты заказчика от кода самой системы и от тестов разработчиков, чтобы не возникало путаницы. Как вариант, можно предложить разместить тесты заказчика в отдельном каталоге.
  2. Пакеты выстраиваются в иерархию, соответствующую иерархии бизнес-процессов, происходящих в системе.
  3. Каждый тестовый сценарий в рамках варианта использования помещается в отдельный класс/файл. Это позволяет получить список тестируемых альтернатив варианта использования просто просмотрев имена файлов в соответствующем пакете. Разумеется, это возможно только если имя класса с тестами является достаточно содержательным.
  4. Имена для тестов должны задаваться в понятных заказчику терминах, соответствующих предметной области системы.
В том случае, если тесты заказчика пишутся на Java, то, следуя этим правилам, можно получить довольно полное представление о существующих тестах путем генерации JavaDoc. А если классы с тестовыми сценариями еще и содержат формальное описание соответствующего сценария, то о лучшей документации не приходится и мечтать. И что весьма важно - эта документация находится рядом с самими тестами, и поэтому вероятность ее своевременного обновления является весьма высокой.

Программные каркасы для написания тестов заказчика

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

Такой программный каркас должен позволять:

  • документировать тестовые сценарии
  • организовывать тесты в иерархию по бизнес-процессам, вариантам использования и тестовым сценариям
  • находить тесты по их положению в иерархии.

Иногда для решения таких задач в качестве языка для подготовки тестовых сценариев и данных используют XML.

Заключение

Итак, в данной статье были рассмотрены вопросы организации и именования тестов двух типов: тестов заказчика, и тестов, которые пишут разработчики (модульные и интеграционные). При этом мы обозначили и постарались решить ряд проблем, встающих перед разработчиком, в частности:
  • выяснение того, был ли написан тест для некоторой функциональности
  • выбор имени для нового теста, и соответствующей тестовой конфигурации
  • упрощение разделения кода тестов и кода системы при ее поставке
  • документирование тестов

В качестве общих рекомендаций для организации тестов было предложено:

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

Ссылки по теме

  1. Библиотека для тестирования Java-программ JUnit: http://www.junit.org
  2. Набор библиотек для тестирования под многие языки программирования: http://www.xprogramming.com/software.htm
  3. Списки рассылки по вопросам тестирования и рефакторинга:
  4. Сайт по экстремальному программированию, на котором обсуждаются многие вопросы, касающиеся тестирования: http://www.xprogramming.ru

kirsa March 30, 2003 8:56 PM

Комментарии

Стоило бы упомянуть http://opensourcetesting.org/
С уважением,
Валентин

Опубликовано: V. Heinitz в October 30, 2006 12:55 AM

В качестве "Программных каркасов для написания тестов заказчика" можно использовать http://izh-test.sourceforge.net/russian/what_is_it_for.html

Опубликовано: Виктор в November 1, 2006 7:50 AM

Очень полезная статья, большое спасибо автору.
Для тестирования веб-приложений можно выделить еще Selenium http://www.openqa.org/selenium/ (Появился он позже написания статьи). Его премиущества относительно упомянутых - то, что тесты исполняются непосредственно в браузере.
Тесты предпологается писать на языке разметки HTML (тест - html таблица), однако есть масса расширений, которые позволяют писать тесты на Java, PHP, Pythonm, Perl и тд. Есть IDE для написания тестов как плагин для Firefox.

На русском видел видел описание селениума здесь: http://wiki.agiledev.ru/doku.php?id=acceptance_testing:selenium

Опубликовано: Андрей Ершов в April 21, 2007 3:52 PM

Хорошая статья. Поставил ссылку на своем сайте doriangray.com.ua .

Selenium заинтересовал, так что будем смотреть.

Опубликовано: Сергей Суздальцев в December 21, 2007 5:09 PM

Сделать комментарий




Запомнить меня?