ShvetsGroup

 

Captions

RSS Следите за нашим блогом и будьте в курсе последних новостей.

 

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

0 комментариев

Введение в 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, '<html><body><pre>'.print_r($variable, true).'</pre></body></html>');
    $this->pass("$description: Contents of result page are ".l('here', $output_path));
  }
}

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