Введение в 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(). Пример:
$params = array('sorting' => 'sorting_value');
$this->drupalGet('find/wine-ratings', array('query' => $params)); - Тесты доступны и для неактивных модулей.
- Создание типов и т.п. стоит выносить в отдельный модуль, и прописывать необходимые процедуры в 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, ''.print_r($variable, true).'');
$this->pass("$description: Contents of result page are ".l('here', $output_path));
}
}
Got anything to add?