Юнит тестирование приложений на Zend Framework’е

Zend Framework - Test Passed

Если вы изучаете 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 является предком для всех тесткейсов:

[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();
    }
}

Написание Тестов

Первый блин

Стоит начать с минимума – проверим, что у нас тесты запускаются и работают, создадим простой тест:

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/ и тама мы должны увидеть что-то вроде:
PHPUnit Coverage

Если вы ничего не увидели – значит – либо директория 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:

DownloadlessonPHPUnit + ZF

Ссылки

23 thoughts on “Юнит тестирование приложений на Zend Framework’е”

  1. Работаю с PHP более года и не сталкивался с PHPUnit, знал, что такое есть, что это полезная вещь, но работать к сожалению не доводилось. Спасибо за статью

  2. Хорошая статья ;) Как бот ответил, но она(статья) действительно интересная и актуальная %)
    зы: SyntaxHighlighter – спасибо, такая клёвая вещица, наверное удобнее пока не видел?

  3. pear install phpunit/PHPUnit

    На самом деле pear install -all phpunit/PHPUnit, так как по умолчанию он не тянет зависимости.

  4. C:\>cd “C:\Program Files\Zend\Apache2\htdocs\zf\tests”
    C:\Program Files\Zend\Apache2\htdocs\zf\tests>phpunit

    Проходит некоторое время и желаемый результат о проведенных тестах не появляется.

    C:\>cd “C:\Program Files\Zend\Apache2\htdocs\zf\tests”
    C:\Program Files\Zend\Apache2\htdocs\zf\tests>phpunit ./

    Аналогичная ситуация. Возможно эту у меня косяки с php / phpunit, но:

    C:\>phpunit –version
    PHPUnit 3.3.17 by Sebastian Bergmann.

    C:\>php -v
    PHP 5.2.9 (cli) (built: May 26 2009 17:47:39)
    Copyright (c) 1997-2009 The PHP Group
    Zend Engine v2.2.0, Copyright (c) 1998-2009 Zend Technologies
    with Zend Extension Manager v5.1, Copyright (c) 2003-2009, by Zend Technologies
    with Zend Guard Loader v3.3, Copyright (c) 1998-2009, by Zend Technologies [loaded] [licensed] [enabled]
    with Zend Data Cache v4.0, Copyright (c) 2004-2009, by Zend Technologies [loaded] [licensed] [disabled]
    with Zend Utils v1.0, Copyright (c) 2004-2009, by Zend Technologies [loaded] [licensed] [enabled]
    with Zend Optimizer+ v4.0, Copyright (c) 1999-2009, by Zend Technologies [loaded] [licensed] [disabled]
    with Zend Debugger v5.2, Copyright (c) 1999-2009, by Zend Technologies [loaded] [licensed] [enabled]

    C:\>ver
    Microsoft Windows XP [Версия 5.1.2600]

    К тому же, то что паритачено в архиве там корневая директория – “zf”, в примере упоминается “zf-project”. Собственно ничего не работает. Сейчас под рукой нет другого набора тестов чтобы проверить на валидность конфигурацию системы, но пример вообще проверялся на валидность?

    1. Использовал PHPUnit 3.4b3 – на совместимость с 3.3 не проверял…

      Попробуй так:

      > phpunit . --configuration phpunit.xml
      

      Или даже используя полные пути:

      > phpunit "C:\Program Files\Zend\Apache2\htdocs\zf\tests" --configuration "C:\Program Files\Zend\Apache2\htdocs\zf\tests\phpunit.xml"
      

      Тесты проверялись на LAMP, вот мой репорт вживую: http://zf.dark.php.nixsolutions.com/tests/report/

      1. Если в текущей директории есть файл phpunit.xml, то все параметры можно опускать, phpunit будет ожидать их в xml. Скорее проблема каким-то образом тогда связана с моей текущей конфигурацией, так как до Zend Server CE делался WAPM, отдельной установкой каждого из компонентов, и уже всплывали проблемы из-за старых переменных окружения (PHP_* & PATH). На sf.net аналогичный пример запустился без особых сложностей, единственная проблема была связана с pear, и решилась созданием локальной (в дополнение к системной) установки pear и использование локального инсталлятора, так как PHPUnit требователен к версии PEAR.

      1. Не уверен что по этой причине, скорее всего из-за того что xDebug нужно грузить до Zend Extension Manger, нужно будет проверить в ближайшее время.

  5. Pingback: progg.ru
  6. Антон, здравствуйте,

    а вы не в курсе как можно потестировать значение в поле ввода?

    Например:

    Что в этом случае использовать? Или как-то настроить assertQueryContentContains?

    Заранее спасибо за разъяснения.

  7. в предыдущем посте где например было
    html
    input type=”text” name=”test” id=”test” value=”1″ /
    html

    с угловыми скобками

  8. Полезный пост, спасибо! Раньше не использовал файл конфигурации тестов в PHPUnit. Подскажите, а есть ли возможность запускать одиночные тесты при использовании глобального файла конфигурации?

    1. В phpdoc классе теста пишешь

      /**
      * @group mysingletest
      */
      class ….Test….

      а запускаешь через phpunit –group mysingletest

  9. Привет. Заметил, что у тебя в ручной настройке 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).
    ———-

    Что скажешь?

  10. Может вы сможете помочь. Пытаюсь запустить тест, но выкидывает ошибку. В чём может быть проблема?

    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

    1. Как решил эту проблему? С файлом “phpunit.xml”:

      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

  11. Такое бывает, если phpunit.xml в виндовой кодировке.

  12. Сделала все как написано.. вся иерархия папок.. файлов и их содержимого… но когда набираю в консоле в папке \tests phpunit – получаю ошибку – Could not open include file: .\pear\PHPUnit2\TextUI\TestRunner.php… Подскажите как решить эту проблему, пожайлуста

    1. Выполни в консоле:

      phpunit –version

      Путь “.\pear\PHPUnit2\TextUI\TestRunner.php” выглядит дико при условии что PHPUnit уже 3.5.10.

  13. Установка через pear сработала только с такой последовательностью:

    pear channel-update pear.php.net
    pear upgrade pear
    pear channel-discover pear.phpunit.de
    pear channel-discover components.ez.no
    pear channel-discover pear.symfony-project.com
    pear install phpunit/PHPUnit

Comments are closed.