Юнит тестирование приложений на Zend Framework’е // PHP
Если вы изучаете PHP больше года, то, наверняка, уже сталкивались с юнит тестированием, и даже писали тесты, ну а в этой статье я приведу лишь “рецепт” для быстрого приготовления тестов на Zend Framework’е.
Установка
Для тестирование приложений на Zend Framework’е используется компонент самого фреймворка Zend_Test, который в свою очередь является лишь потомком PHPUnit’a, и вот его нам надо будет еще установить, детально процесс установки PHPUnit’a описан на домашней странице проекта: Chapter 3. Installing PHPUnit, тут выложу лишь краткий пересказ:
Первый способ заключается в использовании PEAR инсталлера:
pear channel-discover pear.phpunit.de pear install phpunit/PHPUnit
Второй – вручную:
- скачиваем архив с страницы https://phpunit.de/ и распаковываем его в папку, которая включена в include_path (см. php.ini)
- переименовываем phpunit.php в phpunit
- заменяем строку @php_bin@ на путь к PHP (например /usr/bin/php)
- закидываем этот файл ко всем бинарникам, и выполняем команду chmod +x phpunit
- в файле PHPUnit/Util/Fileloader.php заменяем строку @php_bin@ на путь к PHP (например /usr/bin/php)
Структура проекта
Приведу пример структуры каталога для проекта созданного с использование Zend_Tool (дабы не заблудиться, структура каталога tests напоминает структуру всего приложения):
project |-- application |-- data |-- docs |-- library |-- public `-- tests |-- application | |-- models | | `-- PageTest.php | |-- controllers | | |-- IndexControllerTest.php | | `-- ErrorControllerTest.php | |-- ControllerTestCase.php | `-- bootstrap.php |-- library | `-- bootstrap.php `-- phpunit.xml
Кто забыл – проект создаем следующим образом:
zf create project zf-project
Конфигурационный файл phpunit.xml
Для организации тестирования создадим конфигурационный файл phpunit.xml, phpunit необходимо будет запускать в папке tests:
$~/zf-project/tests/phpunit ./
Описание настроек см. в документации
<?xml version="1.0" encoding="UTF-8" ?> <phpunit bootstrap="./application/bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="true" syntaxCheck="true"> <!-- запускаем все тесты из корневой директории --> <testsuite name="Main Test Suite"> <directory>./</directory> </testsuite> <filter> <!-- не смотрим на следующие директории --> <blacklist> <directory suffix=".php">/usr/share/php</directory> <directory suffix=".php">../tests</directory> </blacklist> <!-- смотрим лишь на следующие директории --> <whitelist> <directory suffix=".php">../application</directory> <directory suffix=".php">../library</directory> <exclude> <directory suffix=".phtml">../application</directory> <file>../application/Bootstrap.php</file> </exclude> </whitelist> </filter> <logging> <!-- логирование и создание отчета --> <log type="coverage-html" target="./report" charset="UTF-8" yui="true" highlight="true" lowUpperBound="35" highLowerBound="70"/> </logging> </phpunit>
Инициализация в файле bootstrap.php
В файле phpunit.xml указан bootstrap файл, он отвечает за загрузку нашего приложения, и лишь немногим отличается от привычного index.php:
<?php // Define path to application directory defined('APPLICATION_PATH') || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../../application')); // Define application environment defined('APPLICATION_ENV') || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'testing')); // Ensure library/ is on include_path set_include_path(implode(PATH_SEPARATOR, array( realpath(APPLICATION_PATH . '/../library'), get_include_path(), ))); /** Zend_Application */ require_once 'Zend/Application.php'; require_once 'ControllerTestCase.php'; [/code] <h3>ControllerTestCase.php</h3> Класс ControllerTestCase является предком для всех тесткейсов: <?php require_once 'Zend/Application.php'; require_once 'Zend/Test/PHPUnit/ControllerTestCase.php'; abstract class ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase { /** * @var Zend_Application */ protected $_application; public function setUp() { // указываем функцию, которая будет выполнена до запуска тестов $this->bootstrap = array($this, 'appBootstrap'); parent::setUp(); } public function appBootstrap() { // инициализируем наше приложение $this->_application = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini' ); $this->_application->bootstrap(); } }
Написание Тестов
Первый блин
Стоит начать с минимума - проверим, что у нас тесты запускаются и работают, создадим простой тест:
class IndexControllerTest extends ControllerTestCase { public function testTestAction() { $this->assertTrue(true); } }
Если мы ничего не забыли, то увидим что-то вроде:
PHPUnit 3.4.0beta3 by Sebastian Bergmann. Time: 0 seconds OK (1 tests, 1 assertions) Generating code coverage report, this may take a moment.
Еще чуть-чуть блинов
Теперь немного усложним задачу - будем проверять, туда ли мы попали, для этого будем использовать следующие методы:
- dispatch - указываем URL куда идем
- assertModule - проверяем тот ли модуль, который нам нужен
- assertController - проверяем контроллер
- assertAction - проверяем экшен
class IndexControllerTest extends ControllerTestCase { public function testIndexAction() { $this->dispatch('/'); $this->assertModule('default'); $this->assertController('index'); $this->assertAction('index'); } }
Тоже самое сделаем для ErrorController'а:
class ErrorControllerTest extends ControllerTestCase { public function testErrorURL() { $this->dispatch('foo'); $this->assertModule('default'); $this->assertController('error'); $this->assertAction('error'); } }
Запускаем тесты:
PHPUnit 3.4.0beta3 by Sebastian Bergmann. Time: 0 seconds OK (3 tests, 7 assertions) Generating code coverage report, this may take a moment.
Можно теперь взглянуть на отчет - для этого идем по адресу http://%project%/tests/reports/ и тама мы должны увидеть что-то вроде:
Если вы ничего не увидели - значит - либо директория tests/reports не writable, либо XDebug у вас не установлен.
Проверяем DOM
Теперь создадим новый экшен:
zf create action about index
Добавим функционала для вывода сообщения во view:
public function aboutAction() { // action body $message = $this->_getParam('m'); if ($message) { $this->view->message = $message; } else { $this->view->message = "no message"; } }
Изменим представление:
<h2 id="message"><?= $this->message ?></h2>
Напишем еще немного тестов:
class IndexControllerTest extends ControllerTestCase { // проверяем что данный экшен доступен public function testAboutAction() { $this->dispatch('/index/about'); $this->assertModule('default'); $this->assertController('index'); $this->assertAction('about'); } // проверяем поведение экшена по умолчанию public function testAboutNoMessageAction() { $this->dispatch('/index/about'); $this->assertModule('default'); $this->assertController('index'); $this->assertAction('about'); $this->assertResponseCode(200); // на странице должен присутствовать элемент с ID="messages" в количестве 1 штука $this->assertQueryCount('#message', 1); // и в нем должен быть текст "no message" $this->assertQueryContentContains('#message', "no message"); } // проверяем поведение экшена при входящих параметрах public function testAboutWithMessageAction() { $this->dispatch('/index/about/m/true-lya-lya'); $this->assertModule('default'); $this->assertController('index'); $this->assertAction('about'); $this->assertResponseCode(200); $this->assertQueryCount('#message', 1); // текст теперь должен быть "true-lya-lya" $this->assertQueryContentContains('#message', "true-lya-lya"); } // альтернативный способ передачи параметров в реквест // данный тест дублирует предыдущий public function testAboutWithMessageAltAction() { $this->getRequest() ->setParams(array("m" => "true-lya-lya")) ->setMethod('GET'); $this->dispatch('/index/about/'); $this->assertModule('default'); $this->assertController('index'); $this->assertAction('about'); $this->assertResponseCode(200); $this->assertQueryCount('#message', 1); $this->assertQueryContentContains('#message', "true-lya-lya"); }
Описание assert'ов по DOM'у можно найти в документации:
Тестирование аутентификации пользователей
Для начала нам понадобиться таки написать простую систему аутентификации, создадим новый контроллер Login:
<?php /** * PagesController for default module * * @category Application * @package Default */ class LoginController extends Zend_Controller_Action { public function indexAction() { $this->view->login = false; // проверка не залогинен ли пользователь if (!Zend_Auth::getInstance()->getIdentity()) { // берем форму логина и закидываем во view $form = $this->_getLoginForm(); $this->view->form = $form; // если форма была отправлена, мы должны её проверить if ($this->_request->isPost()) { $formData = $this->_request->getPost(); // проверяем форму if ($form->isValid($formData)) { // проверяем входные данные $result = $this->_authenticate($form->getValue('realm'), $form->getValue('username'), $form->getValue('password')); if ($result->isValid()) { // запомним пользователя на 2 недели if ($form->getValue('rememberMe')) { Zend_Session::rememberMe(60*60*24*14); } // отправляем на главную $this->_redirect('/'); } else { // failure: выводим сообщение о ошибке $this->view->error = 'Authorization error. Please check login or/and password'; } } else { $form->populate($formData); } } } else { $this->view->login = true; $this->view->username = Zend_Auth::getInstance()->getIdentity(); } } // генерация формы логина private function _getLoginForm() { $form = new Zend_Form(); $form->setMethod('POST'); $form->setName('userLoginForm'); $username = new Zend_Form_Element_Text('username'); $username->setLabel('User name') ->setRequired(true) ->addFilter('StripTags') ->addFilter('StringTrim') ->addValidator('Alnum') ->addValidator('StringLength', false, array(3, 24)); $password = new Zend_Form_Element_Password('password'); $password->setLabel('Password') ->setRequired(true) ->setValue(null) ->addValidator('StringLength', false, array(6)); $realm = new Zend_Form_Element_Select('realm'); $realm->setLabel('Role') ->addMultiOptions(array('user'=>'User', 'admin'=>'Admin')) ->setRequired(true) ->setValue('user'); $rememberMe = new Zend_Form_Element_Checkbox('rememberMe'); $rememberMe->setLabel('Remember Me'); $submit = new Zend_Form_Element_Submit('submit'); $submit->setLabel('Login'); $form->addElements(array($realm, $username, $password, $rememberMe, $submit)); return $form; } // аутентификация - самая простая - используя Digest Adapter protected function _authenticate($realm, $login, $password) { $authAdapter = new Zend_Auth_Adapter_Digest(APPLICATION_PATH . '/configs/auth', $realm, $login, $password); $result = $authAdapter->authenticate(); if ($result->isValid()) { // success: сохраняем роль пользователя в Zend_Auth Zend_Auth::getInstance()->getStorage()->write($authAdapter->getRealm()); } return $result; } // разлогиниваемся public function logoutAction() { Zend_Auth::getInstance()->clearIdentity(); $this->_redirect('/'); } }
В комплекте добавим еще и view:
<div id="login"> <?php if ($this->login) :?> <h2>Username</h2> You are logged in as <strong class="username"><?php echo $this->username ?></strong> <?php else : ?> <h2>Login</h2> <!-- выводим ошибку --> <?php if(!empty($this->error)) :?> <div id="error"><?php echo $this->escape($this->error);?></div> <?php endif; ?> <?php echo $this->form; ?> <?php endif; ?> </div>
Теперь необходимо написать тесты для этих контроллеров, но начну я с эмуляции авторизации в юнит-тестах - добавлю метод _doLogin в ControllerTestCase:
protected function _doLogin($realm, $login, $password) { $authAdapter = new Zend_Auth_Adapter_Digest(APPLICATION_PATH . '/configs/auth', $realm, $login, $password); $result = $authAdapter->authenticate(); if ($result->isValid()) { // success: сохраняем роль пользователя в Zend_Auth Zend_Auth::getInstance()->getStorage()->write($authAdapter->getRealm()); } }
Внимание! Для тестирования не следует использовать реальные данные, для данного примера лучше использовать либо тестовый файл аутентификации, либо создать mock для класса Zend_Auth
Теперь стоит проверить как мы логинимся/логаутимся:
class LoginControllerTest extends ControllerTestCase { // логинимся "правильным" данными public function testTrueUserLoginAction() { // эмулируем отправку формы $this->getRequest() ->setMethod('POST') ->setPost(array( "realm" => "user", "username" => "user", "password" => "123456", "rememberMe"=>1)); $this->dispatch('/login/'); // аутентификация должна пройти успешно, идентифицироваться мы должны как user $this->assertEquals(Zend_Auth::getInstance()->getIdentity(), 'user'); // мы должны быть перенаправлены на главную страницу $this->assertRedirectTo('/'); } // логинимся "неправильным" данными public function testFalseUserLoginAction() { $this->getRequest() ->setMethod('POST') ->setPost(array( "realm" => "user", "username" => "user", "password" => "654321", "rememberMe"=>0)); $this->dispatch('/'); // ищем в доме элемент с ID="error" и контентом 'Authorization error. Please check login or/and password' // лучше использовать assertQueryCount $this->assertQueryContentContains('#error', 'Authorization error. Please check login or/and password'); } public function testLogoutAction() { // логинимся $this->_doLogin('admin', 'admin', '123456'); // вызываем логаут $this->dispatch('/login/logout/'); // теперь мы должны быть "забыты" Zend_Auth'ом $this->assertNull(Zend_Auth::getInstance()->getIdentity()); } }
Напишем еще один контроллер - Admin - с очень простой логикой - если заходит не админ - то его должно перенаправит на страницу denied:
class AdminController extends Zend_Controller_Action { public function indexAction() { // проверяем пользователя на принадлежность к админам if (Zend_Auth::getInstance()->getIdentity() !== 'admin') { // если нет - то отправляем на страницу ошибки $this->_forward('denied','error'); } } }
Тесты тоже будут простенькими:
class AdminControllerTest extends ControllerTestCase { public function testIndexAction() { $this->dispatch('/admin/'); $this->assertModule('default'); $this->assertController('error'); $this->assertAction('denied'); } public function testIndexUnderAdminAction() { $this->_doLogin('admin', 'admin', '123456'); $this->dispatch('/admin/'); $this->assertModule('default'); $this->assertController('admin'); $this->assertAction('index'); } }
Результат:
PHPUnit 3.4.0beta3 by Sebastian Bergmann. Time: 2 seconds OK (13 tests, 39 assertions) Generating code coverage report, this may take a moment.
Скачать
Вы можете скачать данный пример по ссылке ниже, в архиве нет Zend Framework'a, пример работает с версией 1.8.4, версия PHPUnit'a - 3.4.0b:
Ссылки
- Материал к данной статье: Zend Framework and Unit testing
- Скрин-каст: Unit Testing with the Zend Framework with Zend_Test and PHPUnit
- Zend_Test - официальный мануал в скудном состоянии
- An Introduction to the Art of Unit Testing in PHP
- PHPUnit: Testing Zend Framework Controllers ( перевод)
- Setting up your Zend_Test test suites
- Automatic testing of MVC applications created with Zend Framework
- Автоматизированное тестирование Zend Framework приложений
- Топик на форуме zendframework.ru