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

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



    Статья носит исключительно практический характер и расчитана на то что вы имеете представление о 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

    Share post

    Comments 5

      +7
      Черт побери, я это сделал ))
        0
        лисапедист, «прежде чем что-то делать — погугли!»
        anton.shevchuk.name/php/unit-tests-zend-framework-application/

        Гараздо более толковый туториал, Вы изобрели по меньшей мере 2 велосипеда:
        1. Забили на phpunit.xml в пользу своего ланчера
        2. Начали изобретать свои оббертки на тесткейсы хотя в ZF они есть (Zend_Test_PHPUnit_ControllerTestCase например), причем встроенные обладают поддержкой интересных ассертов
          +4
          1. Забейте на мой ланчер и напишите свой phpnit.xml
          2. Да я видел все их ассерты и пользуюсь ими.

          Вообще это все частности. Топик не является туториалом на тему как вообще писать тесты, об этом и так много статей в том числе и та что Вы указали. Центральная тема топика создание фикстур и взаимодействие с базой.
        0
        Для этого конкретного туториала важно в какой среде все это делалось.
          0
          Если я правильно понял вопрос, то это делается на dev сервере. База для разработки у нас одна на всех и тесты выполняются в ней же. При текущем количестве тестов (с покрытием выполняются час) это начинает мешать и мы планируем вынести их на отдельный тестовый сервер (виртуальный).

        Only users with full accounts can post comments. Log in, please.