Думаю мало кого из разработчиков можно удивить терминами «модульные тесты» и «разработка через тестирование». Не буду здесь читать проповедь о пользе модульного тестирования и вреде нетестирования. Статья посвящена настройке среды для тестирования приложений на базе Zend Framework. Надеюсь, она поможет начинающим tdd-шникам сэкономить час-другой рабочего времени.
Потребуется
- Zend Framework и приложение на его основе, для которого и будем писать тесты (самая последня версия Zend Framework на данный момент – 1.0.0 RC2);
- TESTS_RUNNER – пакет из фреймворка Limb;
- SimpleTest – фреймоворк для модульного тестирования (т.к. мы будем использовать TESTS_RUNNER, который уже содержит в себе SimpleTest, то скачивать этот фреймворк отдельно не обязательно);
Структура приложения
Мое приложение имеет такую структуру:
--используемые библиотеки
library/
zend_framework/
test_runner/
...
lib/
simpletest/
...
--непосредственно приложение
mysite/
application/
admin/
controllers/
models/
templates/
configuration/
user/
controllers/
models/
User.php
templates/
docs/
tests/
runtests.php
index.php
tests/
application/
models/
cases/
var/
setup.php
Сперва, рассмотрим внутренности папки mysite.
Итак, папка application – ядро приложения, из соображений безопасности я размещаю ее вне папки docs (доступной для посетителей сайта). Внутри application содержится папка admin (для скриптов администраторской части сайта), а в ней, в свою очередь, папки для контроллеров(controllers), моделей(models), шаблонов(templates).
Содержание папки user полностью аналогично admin, здесь хранятся контроллеры, модели и шаблоны для пользовательской части. В configuration я храню конфигурационные файлы(данные для подключения к базе данных, пути к используемым библиотекам и т.д.) Папка docs содержит страницы видимые для посетителей сайта. Папка tests, как несложно догадаться, специально для тестов.
Дальше все просто, в zend_framework храним сам Zend Framework, а в test_runner кладем свежескачанный TESTS_RUNNER.
Настройка конфигурационных файлов
Вкратце о конфигурационных файлах. Их получилось достаточно много.
mysite/application/configuration/database.ini (настройка доступа к БД):
1 2 3 4 5 6 7 | [local] db.adapter = PDO_MYSQL db.config.host = localhost db.config.username = root db.config.password = db.config.dbname = languroo db.config.profiler = true |
mysite/application/configuration/setup.php (общий конфигурационный файл для админ и пользовательской частей):
1 2 3 4 5 6 7 8 9 10 | error_reporting(E_ALL | E_STRICT); date_default_timezone_set('Europe/Kiev'); //set include path for site set_include_path('.' . PATH_SEPARATOR . '/var/www/hosts/library/ZendFramework-1.0.0-RC2/library' . PATH_SEPARATOR . dirname(__FILE__) . '/../library/' . PATH_SEPARATOR . get_include_path() ); require_once('Zend/Loader.php'); |
mysite/application/configuration/user_setup.php
(конфигурационный файл для пользовательской части, аналогичный надо создать для админ части):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //подключаем общий конфиг файл include_once(dirname(__FILE__) . '/setup.php'); set_include_path('.' . PATH_SEPARATOR . dirname(__FILE__) . '/../user/models' . PATH_SEPARATOR . get_include_path() ); //загрузка необходимых классов Zend Framework Zend_Loader::loadClass('Zend_Controller_Front'); Zend_Loader::loadClass('Zend_Controller_Router_Rewrite'); Zend_Loader::loadClass('Zend_View'); Zend_Loader::loadClass('Zend_Config_Ini'); Zend_Loader::loadClass('Zend_Db'); Zend_Loader::loadClass('Zend_Db_Table'); Zend_Loader::loadClass('Zend_Registry'); Zend_Loader::loadClass('Zend_Debug'); Zend_Loader::loadClass('Zend_Auth'); Zend_Loader::loadClass('Zend_Session'); //загружаем конфигурацию базы данных $config = new Zend_Config_Ini( dirname(__FILE__) . '/database.ini', 'local'); $registry = Zend_Registry::getInstance(); $registry->set('config', $config); //прописываем базу данных $db = Zend_Db::factory($config->db->adapter, $config->db->config->toArray()); Zend_Db_Table::setDefaultAdapter($db); $registry->set('db', $db); |
mysite/tests/setup.php (конфигурационный файл для тестов):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | require_once(dirname(__FILE__) . '/../application/configuration/user_setup.php'); //путь к пакету TESTS_RUNNER define('TEST_RUNNER_PATH', dirname(__FILE__) . '/../../library/tests_runner'); define('TEST_DIR_PATH', dirname(__FILE__)); define('LIMB_VAR_DIR', dirname(__FILE__) . '/var'); set_include_path('.' . PATH_SEPARATOR . dirname(__FILE__) . '/../application/' . PATH_SEPARATOR . dirname(__FILE__) . '/application/' . PATH_SEPARATOR . dirname(__FILE__) . '/' . PATH_SEPARATOR . get_include_path() ); //Подключение файлов необходимых для работы TESTS_RUNNER require_once(TEST_RUNNER_PATH . '/common.inc.php'); require_once(TEST_RUNNER_PATH . '/src/lmbTestShellUI.class.php'); require_once(TEST_RUNNER_PATH . '/src/lmbTestWebUI.class.php'); require_once(TEST_RUNNER_PATH . '/src/lmbTestTreeDirNode.class.php'); |
И наконец, в каталоге mysite/docs/tests создадим файл runtests.php (для запуска тестов через веб-интерфейс):
1 2 3 4 5 6 7 8 9 10 | require_once(dirname(__FILE__) . '/../../tests/setup.php'); $node = new lmbTestTreeDirNode(TEST_DIR_PATH . '/cases'); if(PHP_SAPI == 'cli') $ui = new lmbTestShellUI($node); else $ui = new lmbTestWebUI($node); $ui->run(); |
Тестирование
Перейдем непосредственно к тестированию. Проще всего тестировать классы модели. Так как основная цель статьи показать в работе связку Zend Framework c тестовой средой SimpleTest и надстройкой TESTS_RUNNER, поэтому, в этом примере я упущу написание контроллера и создание шаблонов и опишу только тестирование единственного метода в единственном классе модели приложения
Итак начнем с тестов. В каталоге tests содержится подкаталог application, который полностью повторяет структуру каталога mysite/application.
Файл mysite/test/application/user/models/UserTest.php содержит класс для тестирования класса (извините за тавтологию) User, который находится в файле mysite/application/user/models/User.php.
Следуя концепции TDD, сперва начнем писать именно тесты.
mysite/test/application/user/models/UserTest.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | require_once ('user/models/User.php'); class UserTests extends UnitTestCase { function testInsertEmptyUsername() { $user = &new User(); $user_data = array('nickname'=>'', 'gender'=>'male', 'country'=>'us', 'password'=>'new', 'birth_date'=>'2007-05-31', 'email'=>'<a href="mailto:test@mail.ru" class="linkification-ext" title="Linkification: mailto:test@mail.ru">test@mail.ru</a>'); $this->assertFalse($user->insertUser($user_data)); } function testInsertEmptyEmail() { $user = &new User(); $user_data = array('nickname'=>'testUser1', 'gender'=>'male', 'country'=>'us', 'password'=>'new', 'birth_date'=>'2007-05-31', 'email'=>''); $this->assertFalse($user->insertUser($user_data)); } function testCorrectDataInsert() { $user = &new User(); $user_data = array('nickname'=>'testUser1', 'gender'=>'male', 'country'=>'us', 'password'=>'new', 'birth_date'=>'2007-05-31', 'email'=>'<a href="mailto:test@mail.ru" class="linkification-ext" title="Linkification: mailto:test@mail.ru">test@mail.ru</a>'); $this->assertFalse($user->insertUser($user_data)); } } |
Сэкономив место в статье, я поступил несовсем правильно добавив сразу три теста в класс. Идеологически правильно было бы делать все в таком порядке: добавляем один тест, затем пишем минимально функционал, необходимый для того чтобы пройти тест, потом опять добавляем тестовый случай и опять «подгоняем» под него функционал.
Осталось только разместить где-нибудь код создания экземпляра класса UserTests. Для наглядности я поступил следующим образом: в пакете TESTS_RUNNER в папке examples нашел папку cases и скопировал ее в папку tests. Дальше, нужно создать внутри cases, папку user, а в ней файл user_test.php с таким кодом:
1 2 3 | require_once('user/models/UserTests.php'); $userTest = &new UserTests(); |
И, наконец, создаем класс модели.
mysite/application/user/models/User.php
1 2 3 4 5 6 7 8 | class User extends Zend_Db_Table { protected $_name = 'User'; public function insertUser($data) { } } |
Теперь самое время запустить наши тесты.
вводим в браузере url: http://mysite/tests/runtests.php
И получаем в результате нечто вроде такого:
Т.е. мы можем запустить тесты по отдельности либо группой, оооочень удобно, особенно, когда количество тестов перевалит за один
Итак запускам нужный нам user_test и получаем красную полосу, что вполне ожидаемо, так как функционала пока и нет.

Добавляем в метод insertUser, такой код:
1 2 3 4 5 6 7 8 9 10 | $password = md5($data['password']); $birthdate = $data['birth_date']['year'] . '-' . $data['birth_date']['month'] . '-' . $data['birth_date']['day']; $register_date = date('Y-m-d H:i:s'); $data_insert = array('nickname'=>$data['nickname'], 'email'=>$data['email'], 'gender'=>$data['gender'], 'password'=>$password, 'country'=>$data['country'], 'birthdate'=>$birthdate, 'register_date'=>$register_date, ); $result = $this->insert($data_insert); return $result; |
Запустив тест опять получим красную полосу, потому как проверки на пустой никнейм и пустой email не будут пройдены.
Добавим эти проверки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $result = false; if ( !empty($data['nickname']) && !empty($data['email']) ) { $password = md5($data['password']); $birthdate = $data['birth_date']['year'] . '-' . $data['birth_date']['month'] . '-' . $data['birth_date']['day']; $register_date = date('Y-m-d H:i:s'); $data_insert = array('nickname'=>$data['nickname'], 'email'=>$data['email'], 'gender'=>$data['gender'], 'password'=>$password, 'country'=>$data['country'], 'birthdate'=>$birthdate, 'register_date'=>$register_date, ); $result = $this->insert($data_insert); } return $result; |
При запуске тестов получим долгожданную зеленую полосу.
