Если вы изучаете PHP больше года, то, наверняка, уже сталкивались с юнит тестированием, и даже писали тесты, ну а в этой статье я приведу лишь “рецепт” для быстрого приготовления тестов на Zend Framework’е.
Установка
Для тестирование приложений на Zend Framework’е используется компонент самого фреймворка Zend_Test, который в свою очередь является лишь потомком PHPUnit’a, и вот его нам надо будет еще установить, детально процесс установки PHPUnit’a описан на домашней странице проекта: Chapter 3. Installing PHPUnit, тут выложу лишь краткий пересказ:
Первый способ заключается в использовании PEAR инсталлера:
1 2 | 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
Кто забыл – проект создаем следующим образом:
1 | zf create project zf-project |
Конфигурационный файл phpunit.xml
Для организации тестирования создадим конфигурационный файл phpunit.xml, phpunit необходимо будет запускать в папке tests:
1 | $~ /zf-project/tests/phpunit ./ |
Описание настроек см. в документации
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | <?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 является предком для всех тесткейсов: [code lang="php"] <?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(); } } |
Написание Тестов
Первый блин
Стоит начать с минимума – проверим, что у нас тесты запускаются и работают, создадим простой тест:
1 2 3 4 5 6 7 | 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 – проверяем экшен
01 02 03 04 05 06 07 08 09 10 | class IndexControllerTest extends ControllerTestCase { public function testIndexAction() { $this ->dispatch( '/' ); $this ->assertModule( 'default' ); $this ->assertController( 'index' ); $this ->assertAction( 'index' ); } } |
Тоже самое сделаем для ErrorController’а:
01 02 03 04 05 06 07 08 09 10 | 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
Теперь создадим новый экшен:
1 | zf create action about index |
Добавим функционала для вывода сообщения во view:
01 02 03 04 05 06 07 08 09 10 11 | public function aboutAction() { // action body $message = $this ->_getParam( 'm' ); if ( $message ) { $this ->view->message = $message ; } else { $this ->view->message = "no message"; } } |
Изменим представление:
1 | <h2 id="message"><?= $this ->message ?></h2> |
Напишем еще немного тестов:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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:
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 | <?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:
01 02 03 04 05 06 07 08 09 10 11 12 13 | <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:
01 02 03 04 05 06 07 08 09 10 11 | 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
Теперь стоит проверить как мы логинимся/логаутимся:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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:
01 02 03 04 05 06 07 08 09 10 11 | class AdminController extends Zend_Controller_Action { public function indexAction() { // проверяем пользователя на принадлежность к админам if (Zend_Auth::getInstance()->getIdentity() !== 'admin' ) { // если нет - то отправляем на страницу ошибки $this ->_forward( 'denied' , 'error' ); } } } |
Тесты тоже будут простенькими:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | 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' ); } } |
Результат:
1 2 3 4 | 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
Работаю с PHP более года и не сталкивался с PHPUnit, знал, что такое есть, что это полезная вещь, но работать к сожалению не доводилось. Спасибо за статью
Хорошая статья ;) Как бот ответил, но она(статья) действительно интересная и актуальная %)
зы: SyntaxHighlighter – спасибо, такая клёвая вещица, наверное удобнее пока не видел?
Спасибо за очередной полезный урок!
На самом деле pear install -all phpunit/PHPUnit, так как по умолчанию он не тянет зависимости.
Проходит некоторое время и желаемый результат о проведенных тестах не появляется.
Аналогичная ситуация. Возможно эту у меня косяки с php / phpunit, но:
К тому же, то что паритачено в архиве там корневая директория – “zf”, в примере упоминается “zf-project”. Собственно ничего не работает. Сейчас под рукой нет другого набора тестов чтобы проверить на валидность конфигурацию системы, но пример вообще проверялся на валидность?
Использовал PHPUnit 3.4b3 – на совместимость с 3.3 не проверял…
Попробуй так:
Или даже используя полные пути:
Тесты проверялись на LAMP, вот мой репорт вживую: http://zf.dark.php.nixsolutions.com/tests/report/
Если в текущей директории есть файл phpunit.xml, то все параметры можно опускать, phpunit будет ожидать их в xml. Скорее проблема каким-то образом тогда связана с моей текущей конфигурацией, так как до Zend Server CE делался WAPM, отдельной установкой каждого из компонентов, и уже всплывали проблемы из-за старых переменных окружения (PHP_* & PATH). На sf.net аналогичный пример запустился без особых сложностей, единственная проблема была связана с pear, и решилась созданием локальной (в дополнение к системной) установки pear и использование локального инсталлятора, так как PHPUnit требователен к версии PEAR.
Еще кстати и xdebug не удалось установить с Zend Server. Не грузится расширение (http://xdebug.org/files/php_xdebug-2.0.4-5.2.8.dll) хоть ты тресни.
Zend Debugger и XDebug не дружат…
Не уверен что по этой причине, скорее всего из-за того что xDebug нужно грузить до Zend Extension Manger, нужно будет проверить в ближайшее время.
Антон, здравствуйте,
а вы не в курсе как можно потестировать значение в поле ввода?
Например:
Что в этом случае использовать? Или как-то настроить assertQueryContentContains?
Заранее спасибо за разъяснения.
в предыдущем посте где например было
html
input type=”text” name=”test” id=”test” value=”1″ /
html
с угловыми скобками
Полезный пост, спасибо! Раньше не использовал файл конфигурации тестов в PHPUnit. Подскажите, а есть ли возможность запускать одиночные тесты при использовании глобального файла конфигурации?
В phpdoc классе теста пишешь
/**
* @group mysingletest
*/
class ….Test….
а запускаешь через phpunit –group mysingletest
Привет. Заметил, что у тебя в ручной настройке PHPUnit-a написано менять файл PHPUnit/Util/Fileloader.php, а на официальном сайте написано другое
———-
3. Prepare the PHPUnit/Util/PHP.php script:
a. Replace the @php_bin@ string in it with the path to your PHP command-line interpreter (usually /usr/bin/php).
———-
Что скажешь?
Может вы сможете помочь. Пытаюсь запустить тест, но выкидывает ошибку. В чём может быть проблема?
Fatal error: Uncaught exception ‘PHPUnit_Framework_Exception’ with message ‘Could not load “/mnt/hgfs/www/project/tests/phpunit.xml”.’ in /usr/share/php/PHPUnit/Util/XML.php:216
Stack trace:
#0 /usr/share/php/PHPUnit/Util/XML.php(166): PHPUnit_Util_XML::load(‘?__construct(‘/mnt/hgfs/www/j…’)
#3 /usr/share/php/PHPUnit/TextUI/Command.php(753): PHPUnit_Util_Configuration::getInstance(‘/mnt/hgfs/www/j…’)
#4 /usr/share/php/PHPUnit/TextUI/Command.php(155): PHPUnit_TextUI_Command->handleArguments(Array)
#5 /usr/share/php/PHPUnit/TextUI/Command.php(146): PHPUnit_TextUI_Command->run(Array, true)
#6 /usr/bin/phpunit(54): PHPUnit_TextUI_Command::main()
#7 {main}
thrown in /usr/share/php/PHPUnit/Util/XML.php on line 216
Как решил эту проблему? С файлом “phpunit.xml”:
Такое бывает, если phpunit.xml в виндовой кодировке.
Сделала все как написано.. вся иерархия папок.. файлов и их содержимого… но когда набираю в консоле в папке \tests phpunit – получаю ошибку – Could not open include file: .\pear\PHPUnit2\TextUI\TestRunner.php… Подскажите как решить эту проблему, пожайлуста
Выполни в консоле:
phpunit –version
Путь “.\pear\PHPUnit2\TextUI\TestRunner.php” выглядит дико при условии что PHPUnit уже 3.5.10.
Установка через pear сработала только с такой последовательностью: