Как стать автором
Обновить

Практика внедрения PHPunit

Время на прочтение 7 мин
Количество просмотров 8.6K
Уже достаточно сказано о пользе автоматизированного тестирования (например, тут и тут), но до сих пор многие так и не пишут тестов. Одна из причин, как мне кажется, в том что предлагаемые способы автоматизации тестирования сложнее чем необходимо для большинства случаев. Сегодня я хочу рассказать о том как это сделано у нас.



Статья носит исключительно практический характер и расчитана на то что вы имеете представление о phpunit. Для реализации используются ZF, mysql + innodb, но при желании это можно использовать с любым инструментарием. Дополнительно используется dklab.ru/lib/PHP_Exceptionizer (преобразует нотисы и варнинги в эксепшены).

Подготовка



В дереве проекта создается папка tests со следующей структурой.

d- application
d- models
d- triggers

run.php

Содержимое папок application и models повторяет структуру контроллеров и моделей в проекте. Мы не используем тестовые наборы, поэтому добавляя новый файл с тестами в систему нам не нужно добавлять этот тест в набор. В папке triggers лежат папки по именам таблиц, внутри которых файлы, названные как триггеры.
Файл run.php содержит в себе код необходимый для выполнения тестов: автозагрузка классов, подключение к базе и т.п. Скрипт должен работать и в дев среде и в боевой. Это можно сделать с помощью Zend_Console_Getopt. В конце файла строчки

PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
PHPUnit_TextUI_Command::main();


* This source code was highlighted with Source Code Highlighter.


Для того чтобы они заработали phpunit должен быть установлен в системе и настроен на автозагрузку.

Для запуска тестов(-а) из папки tests можно будет так:
php run.php models/Article (запустятся все тесты из папки Article)
php run.php models/ArticleTest.php (запустятся все тесты из файла ArticleTest.php)

Тестирование моделей



Все тесты наследуются от класса Ext_Db_Table_Test_Abstract, который содержит в себе соединение с базой. Методы setUp и TearDown объявлены как final, а их заменяют _start и _finish. В setUp стартует транзакция, а в tearDown она откатывается. Таким образом каждый тест выполняется в своей транзакции, что избавляет нас от необходимости следить за объектами и чистить базу.

<?
  1. <?php
  2.  
  3. abstract class Ext_Db_Table_Test_Abstract extends PHPUnit_Framework_TestCase
  4. {
  5.   /**
  6.    * @var Zend_Db_Adapter_Abstract
  7.    */
  8.   protected static $_db;
  9.  
  10.   public static function setDbAdapter(Zend_Db_Adapter_Abstract $db = null)
  11.   {
  12.     if (empty($db)) {
  13.       $db = Zend_Db_Table_Abstract::getDefaultAdapter();
  14.     }
  15.  
  16.     self::$_db = $db;
  17.   }
  18.  
  19.   /**
  20.    * Zend_Db_Profiler
  21.    *
  22.    * @return Zend_Db_Profiler
  23.    */
  24.   public function getProfiler()
  25.   {
  26.     return self::$_db->getProfiler();
  27.   }
  28.  
  29.   final public function setUp()
  30.   {
  31.     if (empty(self::$_db)) {
  32.       self::setDbAdapter();
  33.     }
  34.     self::$_db->beginTransaction(); // Каждый тест в своей транзакции!
  35.     $this->_start();
  36.   }
  37.  
  38.   final public function tearDown()
  39.   {
  40.     $this->_finish();
  41.     self::$_db->rollBack();
  42.   }
  43.  
  44.   protected function _start()
  45.   {
  46.   }
  47.  
  48.   protected function _finish()
  49.   {
  50.   }
  51. }
* This source code was highlighted with Source Code Highlighter.


Следующий важный момент это созадние фикстуры. Все статьи посвященные тестированию предлагают для этого использовать расширение PHPUnit и его Database Extension. В целом это правильно, но мы хотели проще.

Для создания тестовых данных существует специальный класс Test_Object, представленный в виде singletone.
Если нам нужна статья мы пишем так $article = Test_Object::getInstance()->addArticle();

  1. public function addArticle(array $data = array())
  2.   {
  3.     $base = array(
  4.       'key' => md5(mt_rand()),
  5.       'content' => md5(mt_rand()),
  6.       'name' => md5(mt_rand()),
  7.       'published' => 1,
  8.       'file_id' => $this->addFile()->file_id, // Создаем зависимый объект
  9.       'created' => now()
  10.     );
  11.     $article_table = Article::getInstance();
  12.     if (empty($data['article_category_id'])) { // Если не передали id категории, то категория создается автоматически
  13.       $base['article_category_id'] = $this->addArticleCategory($data)->article_category_id; // Массив $data передается и в метод создающий категорию.
  14.     }
  15.  
  16.     return $this->_createRow($article_table, $base, $data);
  17.   }
  18.  
  19.   protected function _createRow(Ext_Db_Table_Abstract $table, array $base, array $data = array())
  20.   {
  21.     $data = array_merge($base, $data);
  22.     $row = $table->createRow($data);
  23.     $row->save();
  24.  
  25.     return $row;
  26.   }
* This source code was highlighted with Source Code Highlighter.


Здесь видно что мы легко можем переопределить те данные которые нам нужны передав их в addArticle. Так же метод создает внутри себя объекты от которых зависит (если вы их ему не передали в $data). На данный момент Test_Object содержит около 4000 строк кода из-за большого количества сущностей создающихся в системе. Представьте что было бы используй мы xml файлы. Еще одно преимущество данного подхода в том что создание объектов сосредоточено в одном месте и позволяет легко создавать любой граф объектов. Например нам нужен комментарий к статье, для этого пишется метод addArticleComment(), который внутри себя создает все необходимые объекты addUser, AddArticle и т.д. и возвращает нужный нам коммент.

И теперь сам тест.

  1. <?php
  2.  
  3. class ArticleTest extends Ext_Db_Table_Test_Abstract
  4. {
  5.   public function testFindByKey()
  6.   {
  7.     $article = Test_Object::getInstance()->addArticle();
  8.  
  9.     $row = Article::getInstance()->findByKey($article->key);
  10.     $this->assertEquals($article, $row);
  11.   }
  12. }
* This source code was highlighted with Source Code Highlighter.


Запуск и проверка php run.php models/Article/ArticleTest.php

Ниже приводится пример запуска всех тестов моделей. Всего в нашей системе порядка 1200 тестов и около 4000 проверок (Общее покрытие больше 90%).

php run.php model/
PHPUnit 3.3.12 by Sebastian Bergmann.

E........................................................... 60 / 356
............................................................ 120 / 356
............................................................ 180 / 356
F..........................................................I 240 / 356
............................................................ 300 / 356
........................................................

Time: 42 seconds
FAILURES!
Tests: 356, Assertions: 800, Failures: 1, Errors: 1, Incomplete: 1.


Тестирование контроллеров



Проще понять на примере.
  1. <?php
  2.  
  3. class Frontend_Tender_EditControllerTest extends ControllerTestCase // Класс в котором поднимается окружение acl, routing, и т.п.
  4. {
  5.   public function testAddAction() // Проверяем что страница открывается.
  6.   {
  7.     $this->dispatch('/tender/add');
  8.  
  9.     $this->assertNoErrors(); // Проверяет что plugin error не зарегистрировал ошибок.
  10.     $this->assertModuleFromParams('tender');
  11.     $this->assertControllerFromParams('edit');
  12.     $this->assertActionFromParams('add');
  13.   }
  14.   
  15.   public function testAddActionWithPost() // Проверяем форму
  16.   {
  17.     $data = array(
  18.       'name' => 'name',
  19.       'content' => md5(mt_rand()),
  20.       'phone' => '12345',
  21.       'country_id' => 3159,
  22.       'region_id' => 4312,
  23.       'city_id' => 4400,
  24.       'email' => md5(mt_rand()) . '@testemail.ru',
  25.     );
  26.     $this->getRequest()->setMethod('post')
  27.       ->setParams($data);
  28.  
  29.     $this->dispatch('/tender/add');
  30.  
  31.     $this->assertNoErrors();
  32.  
  33.     $table = Tender::getInstance();
  34.     $row = $table->selectByEmail($data['email'])->fetchRow(); // Об этом я расскажу в следующем топике)
  35.     foreach ($data as $key => $value) {
  36.       $this->assertEquals($value, $row->$key); // Проверяем что в базе наши данные
  37.     }
  38.   }
  39. }
* This source code was highlighted with Source Code Highlighter.


Методы assertModuleFromParams, assertControllerFromParams, assertActionFromParams проверяют $request->getParam(blabla).
Мы активно используем actionStack, а он все время меняет реквест, поэтому проверять $request->getModuleName() нецелесообразно.

Заключение



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

p.s. Первый пост на хабре. Если будет интересно напишу про архитектуру проекта в котором участвую.

p.s.s Эта схема опробована и используется для www.okinfo.ru

Теги:
Хабы:
+6
Комментарии 5
Комментарии Комментарии 5

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн