ShvetsGroup

 

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

0 comments

Введение в 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, который доступен во всех установках. Для этого нам потребуется:

  1. Создать файл теста с именем имя_модуля.test и сохранить его в папке с модулем. Имя файла жестко оговорено в SimpleTest.
  2. Далее создаем сам тест:


    <?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'));
    }
    }

    ?>

  3. Очищаем кеш и идем на страницу 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 предлагает достаточный функционал для решения многих проблем.

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

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

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

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

Маленький бонус

Есть набор мелких и серьезных проблем и вопросов, связанных с тестированием с помощью этого модуля, на которые Вы или натолкнетесь, или нет, но мы Вас предупредили :)

  1. SimpleTest не может тестировать JavaScript, поэтому функционал jQuery, динамические подмены контента и т.п. тестировать не получится :(
  2. Список доступных проверок (Assertions) доступен тут: http://drupal.org/node/265828
  3. Для форм модуля View нужно вызывать $this->drupalGet(), вместо drupalPost(). Пример:
    $params = array('sorting' => 'sorting_value');
    $this->drupalGet('find/wine-ratings', array('query' => $params));
  4. Тесты доступны и для неактивных модулей.
  5. Создание типов и т.п. стоит выносить в отдельный модуль, и прописывать необходимые процедуры в module_name.install.
  6. Если создается отдельный модуль специально для тестирования, то в файле module_name.info стоит добавить hidden = TRUE, после этого модуль может вызываться в тестах, но не будет доступен в общем списке.
  7. Модуль 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?