Введение в Unit-тестирование Drupal

Вы добавили функции на сайт, а через несколько дней заказчик позвонил и рассказал, что ничего не работает. Вы уже 20 часов подряд набиваете код и кучу раз идете и клацаете по формам проверяя чтоб все работало, но мозг уже ничего не воспринимает и в итоге на сайт добавился неработающий фрагмент. А может у Вас сложный модуль с кучей взаимосвязанного функционала, ну или небольшой, но с кучей вариантов выбора. В общем у Вас миллионы причин прийти к автоматизированному тестированию.
Использование автоматизированного тестирования позволяет избавиться от кучи рутинных операций по регулярной проверке работоспособности кода. Для тестирования доступны автозаполнение форм и проверка результата, контроль доступа пользователей к различным разделам сайта и функционалу и многое другое.
Что же нам предлагает для тестирования Drupal?
Тестирование модулей и функциональности в Drupal осуществляется с помощью модуля SimpleTest. Причем, с 7 версии она включена в ядро, поэтому смотреть в другую сторону особого смысла и нет.
Установка
Для установки Вам потребуется установленный Drupal и чтобы на сервере была доступна библиотека php-curl, с помощью которой модулем осуществляется парсинг страниц.
После того, как модуль скопирован на сервер, необходимо выполнить патч ядра, файл патча располагается в корневой папке модуля. Для его применения необходимо выполнить на сервере команду из корневой папки сайта:
patch -p0 < {путь-к-папке-с-модулями}/simpletest/D6-core-simpletest.patch
После этого достаточно активировать его во вкладке с модулями и можно просмотреть список доступных тестов на странице admin/build/testing.
Как работает SimpleTest?
В начале он сканирует папки модулей в поисках доступных тестов, при этом совершенно не важно активен модуль или нет, пользователь видит и сами тесты и может их выполнить не включая соответствующий модуль.
Достигается это благодаря тому, что перед выполнением теста SimpleTest создает виртуальную установку Drupal с которой в последствии и работает. Уже в ней активируются необходимые модули и темы, которые могут отличаться от установки текущего сайта.
Дальше, в процессе тестирования вызываются тестируемые страницы или функции, после чего производится их валидация и генерируется вывод информации об успешности или неуспешности операции.
Кстати, для каждой функции testXXX setUp выполняется каждый раз, перед выполнением теста.
Первый тест
Итак, закончим флуд и перейдем к практике. В первом тесте мы проверим создание материала типа Page, который доступен во всех установках. Для этого нам потребуется:
- Создать файл теста с именем имя_модуля.test и сохранить его в папке с модулем. Имя файла жестко оговорено в SimpleTest.
-
Далее создаем сам тест:
<?php class OurModuleTest extends DrupalWebTestCase { // вспомогательная функция, которой мы будем генерировать текст с блекджеком и пробелами protected function randomText($wordCount = 32) { $text = ''; for ($i = 0; $i < $wordCount; $i++) { $text .= $this->randomString(rand(4, 12)) . ' '; } return $text; } // Информация о тесте, которая отображается на странице тестов. public static function getInfo() { return array( 'name' => 'Page creation test', 'desc' => 'Testing page creation', 'group' => 'Our tests', ); } public function setUp() { // устанавливаем необходимые модули $args = func_get_args(); $modules = array_merge(array('help', 'search', 'menu', 'node'), $args); call_user_func_array(array('parent','setUp'), $modules); // устанавливаем необходимые права доступа $permissions = array('access content', 'create page content', 'delete own page content', 'edit own page content'); // создаем пользователя с этими правами и входим в систему $user = $this->drupalCreateUser($permissions); $this->drupalLogin($user); } // Тестирование создания страницы public function testPageCreation() { $params = array( 'title' => $this->randomName(32), 'body' => $this->randomText(), ); // Вызываем страницу создания Page $this->drupalPost('node/add/page', $params, t('Save')); // Проверяем полученный ввод $this->assertText(t('Page @name has been created.', array('@name' => $params['title'])), t('Page creation')); } } ?>
- Очищаем кеш и идем на страницу admin/build/testing. Теперь там мы наблюдаем раскрывающуюся вкладку "Our tests", в которой доступен один тест "Page creation test". Поставив на нем галочку выполняем его. после выполнения нам доступна информация "19 passes, 0 fails, and 0 exceptions". То, что мы и хотели получить.
Теперь разлогиним пользователя и попробуем после этого выполнить тест. Для этого создадим еще один тест и назовем его testAnonymousPageCreation. От предыдущего теста код будет отличаться только тем, что перед выполнением мы выполним $this->drupalLogout()
// Тестирование создания страницы анонимным пользователем public function testAnonymousPageCreation() { // Разлогиниваем пользователя $this->drupalLogout(); $params = array( 'title' => $this->randomName(32), 'body' => $this->randomText(), ); // Вызываем страницу создания Page $this->drupalPost('node/add/page', $params, t('Save')); // Проверяем полученный ввод $this->assertText(t('Page @name has been created.', array('@name' => $params['title'])), t('Page creation')); }
Теперь результат выполнения 29 passes, 5 fails, and 0 exceptions. Однако это далеко не тот результат, который стоило получать. В данном случае нужно проверить заблокирован ли доступ пользователю к этой странице, это и будет успешным тестом, для этого модифицируем тест:
// Тестирование создания страницы анонимным пользователем public function testAnonymousPageCreation() { // Разлогиниваем пользователя $this->drupalLogout(); // Пытаемся получить необходимую страницу $this->drupalGet('node/add/page'); // Проверяем ответ сервера на ошибку 403 (Access denied) $this->assertResponse(403, t('You have no permitions to this page.')); }
Теперь результат: 30 passes, 0 fails, and 0 exceptions. Отлично, теперь мы точно знаем, что неавторизированный пользователь не может получить доступа к созданию страниц.
Что дальше?
Дальше нужно учить себя писать код сразу же с тестами. SimpleTest предлагает достаточный функционал для решения многих проблем.
Во-первых это помогает формализировать задачу, т.к. для теста нужно прописывать четкие критерии успешности.
Во-вторых ошибка будет выявлена раньше и на ее исправление будет затраченно существенно меньше времени, потому, что в наличии окажется точная информация где и при каких условиях ошибка появилась.
В-третьих исключается масса рутинных операций, в которых легко сделать ошибку и пропустить что-то важное.
Ну и самое главное то, что всегда приятно знать, что все написанное работает так, как и предполагалось.
Маленький бонус
Есть набор мелких и серьезных проблем и вопросов, связанных с тестированием с помощью этого модуля, на которые Вы или натолкнетесь, или нет, но мы Вас предупредили :)
- SimpleTest не может тестировать JavaScript, поэтому функционал jQuery, динамические подмены контента и т.п. тестировать не получится :(
- Список доступных проверок (Assertions) доступен тут: http://drupal.org/node/265828
- Для форм модуля View нужно вызывать $this->drupalGet(), вместо drupalPost(). Пример:
- Тесты доступны и для неактивных модулей.
- Создание типов и т.п. стоит выносить в отдельный модуль, и прописывать необходимые процедуры в module_name.install.
- Если создается отдельный модуль специально для тестирования, то в файле module_name.info стоит добавить
hidden = TRUE, после этого модуль может вызываться в тестах, но не будет доступен в общем списке. - Модуль nodecomment конфликтует с модулем comment, поэтому стоит отредактировать файл profiles\default\default.profile и удалить его из установки по-умолчанию.
Ну и напоследок расширенный вариант класса DrupalWebTestCase, в который добавлен набор дополнительных функций и свойств:
class ExtendedDrupalWebTestCase extends DrupalWebTestCase{ protected $admin_user; protected $users; // вспомогательная функция, которой мы будем генерировать текст с блекджеком и пробелами protected function randomText($wordCount = 32) { $text = ''; for ($i = 0; $i < $wordCount; $i++) { $text .= $this->randomString(rand(4, 12)) . ' '; } return $text; } // Смена текущей темы protected function setTheme($new_theme) { db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' and name = '%s'", $new_theme); variable_set('theme_default', $new_theme); drupal_rebuild_theme_registry(); } // генерация имени файла для вывода, в папку, которая находится вне временных папок SimpleTest и позволяет просматривать данные после очистки. protected function getOutputFile() { $file_dir = file_directory_path(); $file_dir .= './simpletest_output_pages'; if (!is_dir($file_dir)) { mkdir($file_dir, 0777, TRUE); } return "$file_dir/$basename." . $this->randomName(10) . '.html'; } // Запись страницы protected function outputAdminPage($description, $basename, $url) { $output_path = $this->getOutputFile(); $this->drupalGet($url); $rv = file_put_contents($output_path, $this->drupalGetContent()); $this->pass("$description: Contents of result page are ".l('here', $output_path)); } // Запись последнего экранного вывода protected function outputScreenContents($description, $basename) { $output_path = $this->getOutputFile(); $rv = file_put_contents($output_path, $this->drupalGetContent()); $this->pass("$description: Contents of result page are ".l('here', $output_path)); } // Запись переменной в файл protected function outputVariable($description, $variable) { $output_path = $this->getOutputFile(); $rv = file_put_contents($output_path, '<html><body><pre>'.print_r($variable, true).'</pre></body></html>'); $this->pass("$description: Contents of result page are ".l('here', $output_path)); } }

Хотите что-то добавить?