PHPUnit. Тестируем базу данных приложения на Zend Framework

Статьи -> Программирование -> Zend Framework

PHPUnit. Тестируем базу данных приложения на Zend Framework

v:1.0 21.04.2010

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

Структура каталогов

Структура каталогов, описанная в первой статье, дополняется каталогом models, в котором будут располагаться файлы с тестами. В этом каталоге будет подкаталог fixtures, в нем я буду хранить данные для тестирования. Причем общие для всех тестов данные будут помещены непосредственно в этот каталог, а данные, специфичные для тестируемых классов будут располагаться в одноименных каталогах.

Cтруктура каталогов

project
|--lib
   |
   |--application
   |--tests
      |
      |--application
      |  |--bootstrap.php    
      |  |--ControllerTestCase.php
      |  |--DbTestCase.php
      |  |--XmlDataSet.php
      |  |
      |  |--models
      |     |--CategoryTest.php
      |     |--fixtures
      |        |--init.xml  
      |        |--Model_DbTable_Category   
      |           |--addCategory.xml
      |           |--delBeginCategory.xml
      |           |--delEndCategory.xml
      |           |--getCategory.xml
      |           |--updateBeginCategory.xml
      |           |--updateEndCategory.xml
      |  
      |--phpunit.xml

В этом примере я рассмотрю тестирование модели таблицы tcategory. Напишу тесты для функций создания, изменения, удаления, получения категории.

Инфраструктурные файлы

Перейдем к рассмотрению файлов.
Файл bootstrap.php не претерпел принципиальных изменений, лишь добавлено подключение нескольких новых файлов.

Файл bootstrap.php

<?php error_reporting( E_ALL | E_STRICT ); date_default_timezone_set('Europe/Moscow'); define('BASE_PATH', realpath(dirname(__FILE__) . '/../../')); define('APPLICATION_PATH', BASE_PATH . '/application'); define('CONFIG_PATH', APPLICATION_PATH . '/configs/application.ini'); set_include_path(     '.'     . PATH_SEPARATOR . BASE_PATH . '/library'     . PATH_SEPARATOR . get_include_path() ); define('APPLICATION_ENV', 'testing'); require_once 'Zend/Application.php';  require_once 'ControllerTestCase.php'; require_once 'DbTestCase.php'; require_once 'XmlDataSet.php'; * This source code was highlighted with Source Code Highlighter.
Файл phpunit.xml не изменился совсем.
По аналогии с ControllerTestCase создаю класс DbTestCase, от которого будут наследоваться все тесты.

Файл DbTestCase.php
<?php require_once 'Zend/Test/PHPUnit/DatabaseTestCase.php'; abstract class DbTestCase extends Zend_Test_PHPUnit_DatabaseTestCase {     protected $_db;     protected $_model;     protected $_modelClass;     protected $_fixturesDir;         protected $_filesDir;     protected $_initDataSet;          public function setUp()     {       $this->_fixturesDir = dirname(__FILE__).'/models/fixtures/';      $this->_filesDir    = $this->_fixturesDir.$this->_modelClass.'/';      $this->_model      = new $this->_modelClass($this->getAdapter());      parent::setUp();     }     protected function getTearDownOperation()     {         return PHPUnit_Extensions_Database_Operation_Factory::DELETE_ALL();     }     protected function getConnection()     {      if (empty($this->_db))      {           $vApplication = new Zend_Application(APPLICATION_ENV,CONFIG_PATH);           $vApplication->bootstrap();                      $vOptions = $vApplication->getOptions();          $vConfig = new Zend_Config_Ini(CONFIG_PATH,'testing');          $vDbname = $vConfig->resources->db->params->dbname;                   $vDb = $vApplication->getBootstrap()->getPluginResource('db')->getDbAdapter();          $this->_db = $this->createZendDbConnection($vDb, $vDbname);      }         return $this->_db;     }     protected function getDataSet($pFileName=null)     {         if ($pFileName===null)         {             $vFileName = $this->_fixturesDir.'init.xml';         }         else         {             $vFileName = $pFileName;         }         return $this->createXmlDataSet($vFileName);     }          protected function prepareInitData($pInitData)     {      $this->getDatabaseTester()->setDataSet($this->getDataSet($pInitData));      $this->getDatabaseTester()->onSetUp();     }     } * This source code was highlighted with Source Code Highlighter.

Рассмотрим файл более внимательно.
Функция setUp() запускается перед началом каждого теста (см. PHPUnit. Часть 04 Тестовые окружения (Fixtures)).
Ее предназначение подготовить тестовое окружение к работе: проинициализировать переменные, хранящие пути к файлам, и загрузить из xml-файла тестовые данные. Функция getTearDownOperation() определяет операцию, которая будет выполняться после каждого теста (см. PHPUnit. Часть 07 Тестирование базы данных). В данном случае будет выполняться очистка таблиц.
Таким образом, с помощью функций setUp() и getTearDownOperation() для каждого теста создается специальное тестового окружение, которое по завершении работы удаляется. Получается, что тесты работают со своими персональными данными и не влияют друг на друга.

Функция getConnection() устанавливает соединение с базой данных и не требует особых пояснений. Отмечу лишь то, что для тестирования создана специальная база, по структуре полностью повторяющая боевую, имя базы зачитывается из ini-файла, это типовой конфигурационный файл Zend Framework.

Функция protected function getDataSet($pFileName=null) - это реализация обязательно метода класса Zend_Test_PHPUnit_DatabaseTestCase, назначение метода - создать тестовые данные. Для всех тестов предполагается один файл с данными init.xml, однако тест может использовать и свой специфический файл.

Класс Zend_Test_PHPUnit_DatabaseTestCase - это Zend Framework заточенный наследник PHPUnit_Extensions_Database_TestCase.

Функция protected function prepareInitData($pInitData) выполняют установку первоначальных данные, если тесту требуются специальные условия.

Файл init.xml
В этом файле хранится конфигурация среды тестирования. Если тест не использует свой конфигурационный файл, то по умолчанию берется этот.

<?xml version="1.0" encoding="UTF-8"?> <dataset>  <table name="tcategory">   <column>Id</column>   <column>Name</column>   <column>ParentId</column>   <column>Description</column>   <column>OrderNo</column>   <column>Level</column>   <column>FlagHasChildren</column>  </table> </dataset> * This source code was highlighted with Source Code Highlighter.
Как видите, это простой xml-файл, который описывает структуру данных и собственно сами данные, более подробно о конфигурационных файлах написано здесь.
Данные не заданы, поэтому после загрузки этого файла из таблицы tcategory будут удалены все записи.

Класс тестирования

Переходим к тестам.

Файл CategoryTest.php
В этом файле находится основной класс.

<?php require_once APPLICATION_PATH.'/models/DbTable/Category.php'; class CategoryTest extends DbTestCase {  protected $_TableName  = 'tcategory';    public function __construct()  {      $this->_modelClass = 'Model_DbTable_Category';  }    public function testaddCategory()  {     $vDataSet = new XmlDataSet($this->_filesDir.'addCategory.xml');          $this->_model->addCategory(                               $vDataSet->getValue($this->_TableName,0,"Name"),                               $vDataSet->getValue($this->_TableName,0,"ParentId"),                               $vDataSet->getValue($this->_TableName,0,"Description"));     $vExpected = $this->createXmlDataSet($this->_filesDir.'addCategory.xml');     $vActual = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());     $vActual->addTable($this->_TableName);     $this->assertDataSetsEqual($vExpected, $vActual);  }  public function testgetCategory()  {     $this->prepareInitData($this->_filesDir.'getCategory.xml');     $vExpected = new XmlDataSet($this->_filesDir.'getCategory.xml');          $vActual = $this->_model->getCategory($vExpected->getValue($this->_TableName,0,"Id"));     $this->assertEquals($vActual["Id"],                         $vExpected->getValue($this->_TableName,0,"Id"));     $this->assertEquals($vActual["Name"],                         $vExpected->getValue($this->_TableName,0,"Name"));     $this->assertEquals($vActual["ParentId"],                         $vExpected->getValue($this->_TableName,0,"ParentId"));     $this->assertEquals($vActual["Description"],                         $vExpected->getValue($this->_TableName,0,"Description"));  }        public function testupdateCategory()  {     $this->prepareInitData($this->_filesDir.'updateBeginCategory.xml');          $vExpected = new XmlDataSet($this->_filesDir.'updateEndCategory.xml');          $this->_model->updateCategory(                                  $vExpected->getValue($this->_TableName,0,"Id"),                                  $vExpected->getValue($this->_TableName,0,"Name"),                                  $vExpected->getValue($this->_TableName,0,"ParentId"),                                  $vExpected->getValue($this->_TableName,0,"Description"));       $vActual = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());     $vActual->addTable($this->_TableName);     $this->assertDataSetsEqual($this->createXmlDataSet($this->_filesDir.'updateEndCategory.xml'),                               $vActual);  }       public function testdelCategory()  {     $this->prepareInitData($this->_filesDir.'delBeginCategory.xml');     $vExpected = new XmlDataSet($this->_filesDir.'delBeginCategory.xml');          $this->_model->delCategory($vExpected->getValue($this->_TableName,0,"Id"));          $vActual = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());     $vActual->addTable($this->_TableName);     $this->assertDataSetsEqual($this->createXmlDataSet($this->_filesDir.'delEndCategory.xml'),                               $vActual);  }     } * This source code was highlighted with Source Code Highlighter.
Обратите внимание на наименования тестовых методов. Наименование образуется так: test+тестируемый метод (см. PHPUnit. Часть 03 Написание тестов для PHPUnit).
Тестируем класс Model_DbTable_Category, поэтому разумно все конфигурационные файлы поместить в каталог fixtures/Model_DbTable_Category.
Для функций создания и получения категории используется типовой набор первоначальных данных init.xml. А вот для функций удаления и изменения требуются специальные стартовые условия.

Функция testaddCategory()

Это тест функции создания категории. Смысл функции заключается в создании категории с заранее заданными параметрами, после чего состояние базы, т.е. тестируемой таблицы сравнивается с заранее известным. Если состояния совпали, значит функция отработала как надо.
Для обращения к параметрам XML-файла я использую свой класс XmlDataSet, вот его код.

Файл XmlDataSet.php
<?php require_once 'PHPUnit/Extensions/Database/DataSet/XmlDataSet.php'; class XmlDataSet extends PHPUnit_Extensions_Database_DataSet_XmlDataSet {  private $_DataSet;     public function __construct($pXmlFile)  {     $this->_DataSet = new PHPUnit_Extensions_Database_DataSet_XmlDataSet($pXmlFile);      }  public function getValue($pTableName, $pRowIndex, $pColumnName)  {     $vTableColumns = array();     $vTableValues = array();     $this->_DataSet->getTableInfo($vTableColumns, $vTableValues);     return $vTableValues[$pTableName][$pRowIndex][$pColumnName];       } } * This source code was highlighted with Source Code Highlighter.
Подозреваю, что есть и более правильный способ, если знаете - подскажите.

Класс XmlDataSet унаследован от PHPUnit_Extensions_Database_DataSet_XmlDataSet и нужен чтобы исправить непонятную особенность - недоступность метода getTableInfo. Функция getValue нужна для упрощения извлечения данных из xml-файлов.

    А теперь как работает тест:
  • PHPUnit запускает функцию setUp(), переопределенную в DbTestCase, эта функция из файла init.xml зачитывает инициализационные данные для таблицы, т.е. по сути очищает эту таблицу.
  • Запускается сам тест testaddCategory, тест создает категорию и проверяет, что получилось в результате.
  • После завершения теста, независимо от результата выполняется функция tearDown(), в DbTestCase я не стал ее переопределять как setUp(),т.к. в этом нет смысла. tearDown() вызывает функцию getTearDownOperation(), которая переопределена в DbTestCase, эта функция очищает все результаты работы теста.
Таким образом, после выполнения testaddCategory все готово для выполнения других тестов, т.к. база данных находится в первоначальном состоянии.
Изолированность тестов имеет множество плюсов: результат не зависит от последовательности выполнения, можно смело менять тестовые данные любого теста и не бояться повлиять на выполнение других.

Принцип работы других тестов аналогичен тесту создания категории: создается среда тестирования, выполняется функция тестируемой модели, полученный результат сравнивается с эталоном, тестовая среда возвращается к первоначальному состоянию.

Конфигурационные файлы

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

XML файлы выбраны для хранения тестовых данных не случайно (о других возможностях см. PHPUnit. Часть 07 Тестирование базы данных). XML позволяет в очень удобном виде описывать структуры данных и сами данные.

Файл addCategory.xml
Эталонные данные для тестирования создания категории.

<?xml version="1.0" encoding="UTF-8"?> <dataset>  <table name="tcategory">   <column>Id</column>   <column>Name</column>   <column>ParentId</column>   <column>Description</column>   <column>OrderNo</column>   <column>Level</column>   <column>FlagHasChildren</column>   <row>      <value>1</value>      <value>addCategory</value>      <null/>      <value>add Category</value>      <null/>      <null/>      <null/>   </row>  </table> </dataset> * This source code was highlighted with Source Code Highlighter.
Файл delBeginCategory.xml
Первоначальные данные для тестирования удаления категории.
<?xml version="1.0" encoding="UTF-8"?> <dataset>  <table name="tcategory">   <column>Id</column>   <column>Name</column>   <column>ParentId</column>   <column>Description</column>   <column>OrderNo</column>   <column>Level</column>   <column>FlagHasChildren</column>   <row>      <value>1</value>      <value>delCategory</value>      <null/>      <value>del Category</value>      <null/>      <null/>      <null/>   </row>   <row>      <value>2</value>      <value>CategoryAfterDel</value>      <null/>      <value>Category After Del</value>      <null/>      <null/>      <null/>   </row>  </table> </dataset> * This source code was highlighted with Source Code Highlighter.
Файл delEndCategory.xml
Эталонные данные для тестирования удаления категории. Как видите, категория с ID=1 должна быть удалена.
<?xml version="1.0" encoding="UTF-8"?> <dataset>  <table name="tcategory">   <column>Id</column>   <column>Name</column>   <column>ParentId</column>   <column>Description</column>   <column>OrderNo</column>   <column>Level</column>   <column>FlagHasChildren</column>   <row>      <value>2</value>      <value>CategoryAfterDel</value>      <null/>      <value>Category After Del</value>      <null/>      <null/>      <null/>   </row>  </table> </dataset> * This source code was highlighted with Source Code Highlighter.
Файл getCategory.xml
Эталонные данные для тестирования функции получения категории.
<?xml version="1.0" encoding="UTF-8"?> <dataset>  <table name="tcategory">   <column>Id</column>   <column>Name</column>   <column>ParentId</column>   <column>Description</column>   <column>OrderNo</column>   <column>Level</column>   <column>FlagHasChildren</column>   <row>      <value>1</value>      <value>getCategory</value>      <null/>      <value>get Category</value>      <null/>      <null/>      <null/>   </row>  </table> </dataset> * This source code was highlighted with Source Code Highlighter.
Файл updateBeginCategory.xml
Первоначальные данные для тестирования изменения категории.
<?xml version="1.0" encoding="UTF-8"?> <dataset>  <table name="tcategory">   <column>Id</column>   <column>Name</column>   <column>ParentId</column>   <column>Description</column>   <column>OrderNo</column>   <column>Level</column>   <column>FlagHasChildren</column>   <row>      <value>1</value>      <value>updateCategory</value>      <null/>      <value>update Category</value>      <null/>      <null/>      <null/>   </row>  </table> </dataset> * This source code was highlighted with Source Code Highlighter.
Файл updateEndCategory.xml
Эталонные данные для тестирования изменения категории.
<?xml version="1.0" encoding="UTF-8"?> <dataset>  <table name="tcategory">   <column>Id</column>   <column>Name</column>   <column>ParentId</column>   <column>Description</column>   <column>OrderNo</column>   <column>Level</column>   <column>FlagHasChildren</column>   <row>      <value>1</value>      <value>updated</value>      <null/>      <value>updated successfully</value>      <null/>      <null/>      <null/>   </row>  </table> </dataset> * This source code was highlighted with Source Code Highlighter.

Заключение

В этой статье я постарался как можно более подробно осветить основы напиания тестов для тестирования моделей базы данных приложений на Zend Framework. Если что-то осталось непонятным, пишите - постараюсь дополнительно пояснить.

Дополнительная информация

Полезная информация о Zend Framework и PHPUnit:
Русскоязычное Zend Framework сообщество
Серия статей о PHPunit.

Петрелевич Сергей
petrelevich@yandex.ru
www.SmartyIT.ru

Метки: PHP   Web   Zend Framework   PHPUnit   Тестирование  

Комментарии.

Внимание.
Комментировать могут только зарегистрированные пользователи.
Возможно использование следующих HTML тегов: <a>, <b>, <i>, <br>.

Яндекс цитирования Ðåéòèíã@Mail.ru Rambler's Top100