Структура каталогов, описанная в первой статье, дополняется каталогом 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+тестируемый метод.
Тестируем класс 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 файлы выбраны для хранения тестовых данных не случайно (о других возможностях. 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>
|