Уже достаточно сказано о пользе автоматизированного тестирования (например, тут и тут), но до сих пор многие так и не пишут тестов. Одна из причин, как мне кажется, в том что предлагаемые способы автоматизации тестирования сложнее чем необходимо для большинства случаев. Сегодня я хочу рассказать о том как это сделано у нас.
Статья носит исключительно практический характер и расчитана на то что вы имеете представление о 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 должен быть установлен в системе и настроен на автозагрузку.
Для запуска тестов(-а) из папки 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 она откатывается. Таким образом каждый тест выполняется в своей транзакции, что избавляет нас от необходимости следить за объектами и чистить базу.
<?
Следующий важный момент это созадние фикстуры. Все статьи посвященные тестированию предлагают для этого использовать расширение PHPUnit и его Database Extension. В целом это правильно, но мы хотели проще.
Для создания тестовых данных существует специальный класс Test_Object, представленный в виде singletone.
Если нам нужна статья мы пишем так $article = Test_Object::getInstance()->addArticle();
Здесь видно что мы легко можем переопределить те данные которые нам нужны передав их в addArticle. Так же метод создает внутри себя объекты от которых зависит (если вы их ему не передали в $data). На данный момент Test_Object содержит около 4000 строк кода из-за большого количества сущностей создающихся в системе. Представьте что было бы используй мы xml файлы. Еще одно преимущество данного подхода в том что создание объектов сосредоточено в одном месте и позволяет легко создавать любой граф объектов. Например нам нужен комментарий к статье, для этого пишется метод addArticleComment(), который внутри себя создает все необходимые объекты addUser, AddArticle и т.д. и возвращает нужный нам коммент.
И теперь сам тест.
Запуск и проверка php run.php models/Article/ArticleTest.php
Ниже приводится пример запуска всех тестов моделей. Всего в нашей системе порядка 1200 тестов и около 4000 проверок (Общее покрытие больше 90%).
Проще понять на примере.
Методы assertModuleFromParams, assertControllerFromParams, assertActionFromParams проверяют $request->getParam(blabla).
Мы активно используем actionStack, а он все время меняет реквест, поэтому проверять $request->getModuleName() нецелесообразно.
Представленный здесь способ, всего лишь один из возможных. Для нас он показался очень удобным и, можно сказать, что прошел проверку временем. И если вы еще не писали тесты самое время начать это делать).
p.s. Первый пост на хабре. Если будет интересно напишу про архитектуру проекта в котором участвую.
p.s.s Эта схема опробована и используется для www.okinfo.ru
Статья носит исключительно практический характер и расчитана на то что вы имеете представление о 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 она откатывается. Таким образом каждый тест выполняется в своей транзакции, что избавляет нас от необходимости следить за объектами и чистить базу.
<?
- <?php
-
- abstract class Ext_Db_Table_Test_Abstract extends PHPUnit_Framework_TestCase
- {
- /**
- * @var Zend_Db_Adapter_Abstract
- */
- protected static $_db;
-
- public static function setDbAdapter(Zend_Db_Adapter_Abstract $db = null)
- {
- if (empty($db)) {
- $db = Zend_Db_Table_Abstract::getDefaultAdapter();
- }
-
- self::$_db = $db;
- }
-
- /**
- * Zend_Db_Profiler
- *
- * @return Zend_Db_Profiler
- */
- public function getProfiler()
- {
- return self::$_db->getProfiler();
- }
-
- final public function setUp()
- {
- if (empty(self::$_db)) {
- self::setDbAdapter();
- }
- self::$_db->beginTransaction(); // Каждый тест в своей транзакции!
- $this->_start();
- }
-
- final public function tearDown()
- {
- $this->_finish();
- self::$_db->rollBack();
- }
-
- protected function _start()
- {
- }
-
- protected function _finish()
- {
- }
- }
* This source code was highlighted with Source Code Highlighter.
Следующий важный момент это созадние фикстуры. Все статьи посвященные тестированию предлагают для этого использовать расширение PHPUnit и его Database Extension. В целом это правильно, но мы хотели проще.
Для создания тестовых данных существует специальный класс Test_Object, представленный в виде singletone.
Если нам нужна статья мы пишем так $article = Test_Object::getInstance()->addArticle();
- public function addArticle(array $data = array())
- {
- $base = array(
- 'key' => md5(mt_rand()),
- 'content' => md5(mt_rand()),
- 'name' => md5(mt_rand()),
- 'published' => 1,
- 'file_id' => $this->addFile()->file_id, // Создаем зависимый объект
- 'created' => now()
- );
- $article_table = Article::getInstance();
- if (empty($data['article_category_id'])) { // Если не передали id категории, то категория создается автоматически
- $base['article_category_id'] = $this->addArticleCategory($data)->article_category_id; // Массив $data передается и в метод создающий категорию.
- }
-
- return $this->_createRow($article_table, $base, $data);
- }
-
- protected function _createRow(Ext_Db_Table_Abstract $table, array $base, array $data = array())
- {
- $data = array_merge($base, $data);
- $row = $table->createRow($data);
- $row->save();
-
- return $row;
- }
* This source code was highlighted with Source Code Highlighter.
Здесь видно что мы легко можем переопределить те данные которые нам нужны передав их в addArticle. Так же метод создает внутри себя объекты от которых зависит (если вы их ему не передали в $data). На данный момент Test_Object содержит около 4000 строк кода из-за большого количества сущностей создающихся в системе. Представьте что было бы используй мы xml файлы. Еще одно преимущество данного подхода в том что создание объектов сосредоточено в одном месте и позволяет легко создавать любой граф объектов. Например нам нужен комментарий к статье, для этого пишется метод addArticleComment(), который внутри себя создает все необходимые объекты addUser, AddArticle и т.д. и возвращает нужный нам коммент.
И теперь сам тест.
- <?php
-
- class ArticleTest extends Ext_Db_Table_Test_Abstract
- {
- public function testFindByKey()
- {
- $article = Test_Object::getInstance()->addArticle();
-
- $row = Article::getInstance()->findByKey($article->key);
- $this->assertEquals($article, $row);
- }
- }
* 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.
Тестирование контроллеров
Проще понять на примере.
- <?php
-
- class Frontend_Tender_EditControllerTest extends ControllerTestCase // Класс в котором поднимается окружение acl, routing, и т.п.
- {
- public function testAddAction() // Проверяем что страница открывается.
- {
- $this->dispatch('/tender/add');
-
- $this->assertNoErrors(); // Проверяет что plugin error не зарегистрировал ошибок.
- $this->assertModuleFromParams('tender');
- $this->assertControllerFromParams('edit');
- $this->assertActionFromParams('add');
- }
-
- public function testAddActionWithPost() // Проверяем форму
- {
- $data = array(
- 'name' => 'name',
- 'content' => md5(mt_rand()),
- 'phone' => '12345',
- 'country_id' => 3159,
- 'region_id' => 4312,
- 'city_id' => 4400,
- 'email' => md5(mt_rand()) . '@testemail.ru',
- );
- $this->getRequest()->setMethod('post')
- ->setParams($data);
-
- $this->dispatch('/tender/add');
-
- $this->assertNoErrors();
-
- $table = Tender::getInstance();
- $row = $table->selectByEmail($data['email'])->fetchRow(); // Об этом я расскажу в следующем топике)
- foreach ($data as $key => $value) {
- $this->assertEquals($value, $row->$key); // Проверяем что в базе наши данные
- }
- }
- }
* 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