Автор Тема: Разработка приложения с использованием Unit тестов  (Прочитано 24216 раз)

0 Пользователей и 1 Гость смотрят эту тему.

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
Решил следовать принципам TDD. С PHPUnit я в общих чертах знаком, но всерьез её буду использовать впервые. Да и в ZF 1.8 некоторые вещи поменялись. Поэтому рассчитываю на помощь и поддержку сообщества.

Итак, рассказываю, что и как я делал.

Для начала создал проект используя Zend_Tool:
Цитировать
zf create project

Увидел незнакомый файл tests/phpunit.xml. Полез читать документацию:
http://www.phpunit.de/manual/3.4/en/organizing-tests.html#organizing-tests.xml-configuration
http://www.phpunit.de/manual/3.4/en/appendixes.configuration.html
http://weierophinney.net/matthew/archives/190-Setting-up-your-Zend_Test-test-suites.html

В результате получился вот такой конфиг:
<?xml version="1.0" encoding="UTF-8"?>

<phpunit bootstrap="bootstrap.php"
         stopOnFailure="true">
    <testsuite name="Main Test Suite">
        <directory>./</directory>
    </testsuite>

    <filter>
        <whitelist>
            <directory suffix=".php">../library/</directory>
            <directory suffix=".php">../application/</directory>
            <exclude>
                <directory suffix=".phtml">../application/</directory>
            </exclude>
        </whitelist>
    </filter>

</phpunit>


Matthew Weier O'Phinney в своей статье советует использовать TestHelper и подключать его в кейсах. В ней же он пишет, что в будущем у PHPUnit будет возможность подгружать загрузочный файл указанный в xml конфигурации. Собственно в PHPUnit 3.3.9 эта возможность уже есть и в конфиге я её использовал. Только вместо TestHelper'a подключаю test/bootstrap.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''testing');

/** Zend_Application */
require_once 'Zend/Application.php';

// Create application, bootstrap, and run
$application = new Zend_Application(
    
APPLICATION_ENV,
    
APPLICATION_PATH '/configs/application.ini'
);
$application->bootstrap();


Это я передер с дефолтного public/index.php. Только убрал добавление library в include_path, так как она присутствует в дефолтном конфиге:
Цитировать
[production]
phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0
includePaths.library = APPLICATION_PATH "/../library"
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"

[staging : production]

[testing : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1

[development : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1

Пока вроде все запускается и тест пустышка в tests/application/controllers/UserControllerTest.php выполняется успешно:
Цитировать
phpunit --configuration phpunit.xml
PHPUnit 3.3.9 by Sebastian Bergmann.

.

Time: 0 seconds

OK (1 test, 1 assertion)

Сейчас буду пробовать писать тесты для контроллера User (регистрация, авторизация и т.д.).

До этого момента я все правильно делаю? Ещё смущает наличие двух пустых загрузочных файлов в tests/application/bootstrap.php и tests/library/bootstrap.php

Оффлайн lcf

  • Модератор
  • Герой
  • *****
  • Сообщений: 2468
  • Карма: 153
    • Homepage
До этого момента я все правильно делаю?
С конфигами я не пробовал, а так концептуально да, вроде как правильно.

Ещё смущает наличие двух пустых загрузочных файлов в tests/application/bootstrap.php и tests/library/bootstrap.php
Да, пожалуй это единственное что смущает. Например ты разрабатываешь библиотечные классы для своего приложения которые ложишь в папку library - зачем же тебе для их тестирования вот этот код:

$application 
= new Zend_Application(
&
#160; &#160; APPLICATION_ENV,
&#160; &#160; APPLICATION_PATH . '/configs/application.ini'
);
$application->bootstrap();

? Конечно вопрос производительности не стоит, но это в принципе может помешать тестированию да и идеологически не правильно, поэтому я бы твой файл убрал в директорию аппликейшн, а для библиотечных файлов использовал отдельный бутстрап, пустышка для которого и нагенерилась. Если подумать то я в общем бутстрапе для двух этих папок смысла и не вижу то...
« Последнее редактирование: Май 12, 2009, 19:45:36 от lcf »

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
? Конечно вопрос производительности не стоит, но это в принципе может помешать тестированию да и идеологически не правильно, поэтому я бы твой файл убрал в директорию аппликейшн, а для библиотечных файлов использовал отдельный бутстрап, пустышка для которого и нагенерилась. Если подумать то я в общем бутстрапе для двух этих папок смысла и не вижу то...
Логично. Как бы ещё для application и library указать свои xml конфиги, чтоб не запускать каждый раз $application->bootstrap() в setUp'ах тестов контроллеров... В документации не нашел. Методом тыка не подхватывает. Смотрю исходники PHPUnit...

Оффлайн lcf

  • Модератор
  • Герой
  • *****
  • Сообщений: 2468
  • Карма: 153
    • Homepage
так а чо если в каждом каталоге по xml ке создать не покатит что ле? \

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
так а чо если в каждом каталоге по xml ке создать не покатит что ле? \
Пробовал - не прокатило. Пока написал Бергманну и качаю студию. Посмотрю как она свои каркасы создает.

Оффлайн lcf

  • Модератор
  • Герой
  • *****
  • Сообщений: 2468
  • Карма: 153
    • Homepage
так а чо если в каждом каталоге по xml ке создать не покатит что ле? \
Пробовал - не прокатило. Пока написал Бергманну и качаю студию. Посмотрю как она свои каркасы создает.
не понял а по какому принципу оно подхватывает тот из корня папки тест???

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
так а чо если в каждом каталоге по xml ке создать не покатит что ле? \
Пробовал - не прокатило. Пока написал Бергманну и качаю студию. Посмотрю как она свои каркасы создает.
не понял а по какому принципу оно подхватывает тот из корня папки тест???
Вот так:
        if (!isset($arguments['configuration']) && file_exists('phpunit.xml')) {
            
$arguments['configuration'] = realpath('phpunit.xml');
        } 

PHPUnit\TextUI\Command.php
Я так понял, что эта проверка только при запуске. Т.е. можно запускать с ключем --configuration и тыкать его в конфиг или если конфиг называется phpunit.xml, то он подхватывает его сам.
Нашел ещё вот такое решение http://maff.ailoo.net/2009/04/set-up-a-zend-framework-application-using-zend_application-including-phpunit-setup/
Но если можно это сделать через конфиги, то хотелось бы через конфиги...

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
Пришел к такому решению:
Цитировать
|-- tests
    |-- application
        |-- controllers
            `-- IndexControllerTest.php
            `-- phpunit.xml
            `-- UserControllerTest.php
        `-- bootstrap.php       
        `-- phpunit.xml
    |-- library
        `-- bootstrap.php       
        `-- phpunit.xml
    `-- phpunit.xml

test/phpunit.xml
Цитировать
<phpunit bootstrap="./application/bootstrap.php">
    <testsuite name="Main Test Suite">
        <directory>./</directory>
    </testsuite>
</phpunit>

test/application/phpunit.xml
Цитировать
<phpunit bootstrap="./bootstrap.php">
    <testsuite name="Application Test Suite">
        <directory>./</directory>
    </testsuite>
</phpunit>

test/application/bootstrap.php
<?php

// Define path to application directory
defined('APPLICATION_PATH')
    || 
define('APPLICATION_PATH'realpath(dirname(__FILE__) . '/../../application'));

// Define application environment
define('APPLICATION_ENV''testing');

require_once 
'ControllerTestCase.php';


test/application/ControllerTestCase.php
<?php
require_once 'Zend/Application.php';
require_once 
'Zend/Test/PHPUnit/ControllerTestCase.php';

abstract class 
ControllerTestCase extends Zend_Test_PHPUnit_ControllerTestCase
{
    public 
$application;

    public function 
setUp()
    {
        
$this->application = new Zend_Application(
            
APPLICATION_ENV,
            
APPLICATION_PATH '/configs/application.ini'
        
);

        
$this->bootstrap = array($this'appBootstrap');
        
parent::setUp();
    }

    public function 
appBootstrap()
    {
        
$this->application->bootstrap();
    }
}


test/application/controllers/phpunit.xml
Цитировать
<phpunit bootstrap="../bootstrap.php">
    <testsuite name="Controllers Test Suite">
        <directory>./</directory>
    </testsuite>
</phpunit>

test/application/controllers/IndexControllerTest.php
<?php
/**
 * @see TestHelper
 */
require_once realpath(dirname(__FILE__) . '/../bootstrap.php');

/**
 * Test class for IndexController
 * @group Controllers
 */
class IndexControllerTest extends ControllerTestCase
{
    public function 
testIndexAction() {
        
$this->dispatch('/');
        
$this->assertController('index');
        
$this->assertAction('index');
    }

    public function 
testErrorURL() {
        
$this->dispatch('foo');
        
$this->assertController('error');
        
$this->assertAction('error');
    }
}


Таким образом просто набрав в консоли phpunit я могу запускать соответствующие наборы тестов в любой из директорий: tests, tests/application, test/application/controllers, test/library.
« Последнее редактирование: Май 13, 2009, 16:13:14 от stfalcon »

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
Понемногу продвигаюсь дальше.

Значит есть контроллер пользователей:
<?php

class UsersController extends Zend_Controller_Action
{

    public function 
loginAction()
    {
        
$form $this->_getLoginForm();
        
$this->view->form $form;

        if (
$this->_request->isPost()) {
            
$formData $this->_request->getPost();
            if (
$form->isValid($formData)) {
                
// setup Zend_Auth adapter for a database table
                
$authAdapter  = new Zend_Auth_Adapter_DbTable(
                    
Zend_Db_Table::getDefaultAdapter(),
                    
'users',
                    
'username',
                    
'passwordHash',
                    
'MD5(CONCAT(?, passwordSalt))'
                
);

                
// set the input credential values to authenticate against
                
$authAdapter->setIdentity($form->getValue('username'));
                
$authAdapter->setCredential($form->getValue('password'));

                
// do the authentication
                
$auth Zend_Auth::getInstance();

                
$result $auth->authenticate($authAdapter);

                if (
$result->isValid()) {
                    
// success: store database row to auth's storage system
                    
$authData $authAdapter->getResultRowObject(
                        
null,
                        array(
'passwordHash''passwordSalt')
                    );
                    
$auth->getStorage()->write($authData);

                    
// remember user for 2 weeks
                    
if ($form->getValue('rememberMe')) {
                        
Zend_Session::rememberMe(60*60*24*14);
                    }

                    
$this->_redirect('/');
                } else {
                    
// failure: clear database row from session
                    
$this->view->message $this->view->translate('Ошибка авторизации. 
Проверьте правильность ввода логина и пароля'
);
                }
            } else {
                
$form->populate($formData);
            }
        }
    }

    
/**
     * Форма авторизации
     *
     * @return Zend_Form
     */
    
private function _getLoginForm()
    {
        
$form = new Zend_Form();

        
$form->setName('userLoginForm');

        
$username = new Zend_Form_Element_Text('username');
        
$username->setLabel(_('Имя пользователя'))
                 ->
setRequired(true)
                 ->
addFilter('StripTags')
                 ->
addFilter('StringTrim')
                 ->
addValidator('Alnum')
                 ->
addValidator('StringLength'false,
                                array(
Model_Users::minUsernameLength,
                                      
Model_Users::maxUsernameLength));

        
$password = new Zend_Form_Element_Password('password');
        
$password->setLabel(_('Пароль'))
                 ->
setRequired(true)
                 ->
setValue(null)
                 ->
addValidator('StringLength'false,
                                array(
Model_Users::minPasswordLength));

        
$rememberMe = new Zend_Form_Element_Checkbox('rememberMe');
        
$rememberMe->setLabel(_('Запомнить меня'));

        
$submit = new Zend_Form_Element_Submit('submit');
        
$submit->setLabel(_('Войти'));

        
$form->addElements(array($username$password$rememberMe$submit));

        return 
$form;
    }

}

Есть модель:
<?php

/**
 * Модель пользователей
 */
class Model_Users extends Zend_Db_Table_Abstract
{
    
/**
     * Таблица
     * @var string
     */
    
protected $_name 'users';

    
/**
     * Ключ
     * @var string
     */
    
protected $_primary 'id';

    
/**
     * Минимальная длина имени пользователя
     */
    
const minUsernameLength 3;

    
/**
     * Максимальная длина имени пользователя
     */
    
const maxUsernameLength 16;

    
/**
     * Минимальная длинна пароля
     */
    
const minPasswordLength 6;
    
}


И тест для контроллера:
<?php
/**
 * @see TestHelper
 */
require_once realpath(dirname(__FILE__) . '/../bootstrap.php');

/**
 * Test class for UserController
 * @group Controllers
 */
class UsersControllerTest extends ControllerTestCase
{

    
/**
     * Проверка контроллера и дефолтного действия для "/users"
     */
    
public function testCallWithoutActionShouldPullFromIndexAction()
    {
        
$this->dispatch('/users');
        
$this->assertController('users');
        
$this->assertAction('index');
    }

    
/**
     * На странице авторизации должна быть форма авторизации
     */
    
public function testLoginActionShouldContainLoginForm()
    {
        
$this->dispatch('/users/login');
        
$this->assertController('users');
        
$this->assertAction('login');
        
$this->assertQueryCount('form#userLoginForm'1);
    }

    
/**
     * При успешной авторизации происходит редирект на главную страницу
     */
    
public function testValidLoginShouldGoToMainPage()
    {
        
$this->_loginUser('administrator''password');
        
$this->assertRedirectTo('/');
    }

    
/**
     * При неуспешной авторизации редирект не происходит.
     * Выводится форма авторизации и список ошибок
     */
    
public function testInvalidCredentialsShouldResultInRedisplayOfLoginForm()
    {
        
$this->_loginUser('fakeuser''fakepassword');
        
$this->assertNotRedirect();
        
$this->assertQueryCount('form#userLoginForm'1);
    }

    
/**
     * Авторизация пользователя
     *
     * @param string $username
     * @param string $password
     */
    
public function _loginUser($username$password)
    {
        
$this->resetRequest()
             ->
resetResponse();
        
$this->request->setMethod('POST')
                      ->
setPost(array(
                          
'username' => $username,
                          
'password' => $password,
                      ));
        
$this->dispatch('/users/login');
    }

}


Я этот тест рожал часа четыре... В основном по мануалу :)

На чем теперь затык. Думаю как лучше вставлять данные в базу перед началом тестирования? То что сейчас там есть я ручками вставлял.

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
Куда правильнее создание тестовой базы засунуть? В контроллер или в модель? Или лучше отдельно вынести? Если отдельно вынести, то куда?

Посоветуйте :)

Оффлайн lcf

  • Модератор
  • Герой
  • *****
  • Сообщений: 2468
  • Карма: 153
    • Homepage
Куда правильнее создание тестовой базы засунуть? В контроллер или в модель? Или лучше отдельно вынести? Если отдельно вынести, то куда?

Посоветуйте :)
Для тестов? В тесты и засунуть. Можно причем её по tearUp или tearDown обновлять, чтобы тесты в плане структуры и контента бд были независимы. Зачем в контроллере или модели тестовая база? Или я не понял вопрос?

ПС: константы не по стандарту (занудствую ^_^)

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
Куда правильнее создание тестовой базы засунуть? В контроллер или в модель? Или лучше отдельно вынести? Если отдельно вынести, то куда?

Посоветуйте :)
Для тестов? В тесты и засунуть. Можно причем её по tearUp или tearDown обновлять, чтобы тесты в плане структуры и контента бд были независимы. Зачем в контроллере или модели тестовая база? Или я не понял вопрос?

ПС: константы не по стандарту (занудствую ^_^)
В принципе логично. Так и сделаю :)
ПС. Констаты исправил, спасибо, что заметил ;)

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
Продвигается! Главное я просто балдею запуская эти тесты :)
Один раз уже словил ошибку, которая всплыла после небольшого рефакторинга.

Итак. Данные в базе инициализирую так как посоветовал lcf:
class UsersControllerTest extends ControllerTestCase
{
    
    public function 
setUp()
    {
        
parent::setUp();

        
$users = new Model_Users();
        
// clear database
        
$users->delete('');
        
// create administrator
        
$users->createNewUser('administrator''password''administrator');
    }


В процессе рефакторинга вынес в модель добавление нового пользователя в базу и генерацию соли. Кошерно так? Контроллеры ведь могут быть разные, а модель одна...
<?php

/**
 * Модель пользователей
 */
class Model_Users extends Zend_Db_Table_Abstract
{
    
/**
     * Таблица
     * @var string
     */
    
protected $_name 'users';

    
/**
     * Ключ
     * @var string
     */
    
protected $_primary 'id';

    
/**
     * Минимальная длина имени пользователя
     */
    
const MIN_USERNAME_LENGTH 3;

    
/**
     * Максимальная длина имени пользователя
     */
    
const MAX_USERNAME_LENGTH 16;

    
/**
     * Минимальная длинна пароля
     */
    
const MIN_PASSWORD_LENGTH 6;

    
/**
     * Генерация соли
     *
     * @return string
     */
    
public static function _generateSalt()
    {
        
$salt '';
        
$length rand(58); // Длина соли (от 5 до 8 символов)
        
for($i=0$i<$length$i++) {
             
$salt .= chr(rand(33126)); // символ из ASCII-table
        
}

        return 
$salt;
    }

    
/**
     * Добавляем нового пользователя в базу
     *
     * @param string $username
     * @param string $password
     * @param string $role
     *
     * @return void
     */
    
public function createNewUser($username$password$role 'member')
    {
        
$dba $this->getDefaultAdapter();

        
$passwordSalt Model_Users::_generateSalt();
        
$passwordHash = new Zend_Db_Expr(
            
'MD5(CONCAT(
                ' 
$dba->quote($password) . ',
                ' 
$dba->quote($passwordSalt) . '
            ))'
        
);

        
$user = array('createdAt' => new Zend_Db_Expr('NOW()'),
                      
'username' => $username,
                      
'passwordHash' => $passwordHash,
                      
'passwordSalt' => $passwordSalt,
                      
'role' => $role
        
);

        
$this->insert($user);
    }
    
}


Оффлайн lcf

  • Модератор
  • Герой
  • *****
  • Сообщений: 2468
  • Карма: 153
    • Homepage
В процессе рефакторинга вынес в модель добавление нового пользователя в базу и генерацию соли. Кошерно так? Контроллеры ведь могут быть разные, а модель одна...
Не совсем понял, по какому поводу конкретно есть сомнения.

Оффлайн stfalcon

  • Team
  • Герой
  • ***
  • Сообщений: 1129
  • Карма: 54
  • Добрый сокольничий ^_~
    • My name is Tanasiychuk Stepan і це мій блог
В процессе рефакторинга вынес в модель добавление нового пользователя в базу и генерацию соли. Кошерно так? Контроллеры ведь могут быть разные, а модель одна...
Не совсем понял, по какому поводу конкретно есть сомнения.
Я просто раньше в модели вообще никаких методов не писал. Все делал в контроллере. А сейчас задумался над этим и пришел к выводу, что так не совсем правильно...