PHPUnit. Тестируем базу данных приложения на Zend Framework
|
Опубликовано: 24.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';
Файл 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();
}
}
Рассмотрим файл более внимательно.
Функция 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>
Как видите, это простой 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);
}
}
Обратите внимание на наименования тестовых методов. Наименование образуется так: 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];
}
}
Подозреваю, что есть и более правильный способ, если знаете - подскажите.
Класс 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>
Файл 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>
Файл 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>
Файл 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>
Файл 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>
Файл 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>
Заключение
В этой статье я постарался как можно более подробно осветить основы написания тестов для тестирования моделей базы данных приложений на Zend Framework. Если что-то осталось непонятным, пишите - постараюсь дополнительно пояснить.
- Спрашивать почему у вас не работает код, для этого есть тема форума закрепленная за статьей.
- Спрашивать как реализовать ту или иную функциональность, для этого необходимо создать свою тему на форуме.
Комментарии для того чтобы: высказать свое аргументированное мнение о статье, указать какие участки вызывают непонимание, что нужно исправить/улучшить, просто сказать спасибо.
Комментарии имеют древовидную структуру.
Если вы хотите ответить на определенный комментарий - нажмите на ссылку "Ответить" возле этого комментария.

