framework.zend.com
Stable релиз 2.0 / 1.12

Автоматизированное тестирование Zend Framework приложений

К комментариям

Содержание

Введение

Предлагаем вашему вниманию перевод статьи Automated Testing Using Zend Framework, Part 1, автором которой является A.J. Brown. Перевел Рашад Суркин, с небольшой редакцией Александра Махомета.

Автоматизированное тестирование вашего веб – приложения является важным шагом на пути приобретения спокойствия при внесении изменений в программу. А так же является, в определённой степени, гарантом защиты от повторного появления уже исправленных ошибок. С Zend Framework's testing framework (который основан на PHPUnit), Вы сможете создать хороший набор контрольных тестов для вашего приложения, приложив для этого лишь незначительные усилия.

Первая часть этой статьи рассказывает о начальных сведеньях которые необходимы для того чтобы приступить к написанию автоматизированных тестов для вашего Zend Framework приложения

Итак начнём

Пример, который мы рассмотрим чуть позже, основан на реальном контроллере который используется в одном из моих проектов. Этот контроллер обрабатывает такие действия пользователя: вход в систему (login) , выход из системы (logout), регистрацию, и подтверждение регистрации. Тестовая база данных, которая используется в примере повторяет структуру базы данных, которая используется у нас на "production" сервере. Я предполагаю, что вы используете устоявшеюся архитектуру проекта на основе Zend Framework (1.6+), что вы знакомы с Zend_Config, и используете Initializer controller plugin (созданный по умолчанию, при использовании Zend Studio for Eclipse 6.1).

Подготовка приложения

Первым шагом для работы с автоматизированным тестированием является подготовка среды выполнения приложения и настроек. В зависимости от того как было построено ваше приложение, это может повлечь за собой появление глобальных переменных, изменение параметров доступа к базе данных или изменение настроек рабочих директорий приложения. К счастью для нас это не сложная задача, в виду использования Zend_Config и Initializer controller plugin.

Zend_Config позволяет вам использовать "секции", которые наследуют друг друга, что в свою очередь позволяет изменять настройки для различных сред окружения (тестовая, внутренняя, рабочая). Это позволяет хранить все настройки в одном, а не в нескольких файлах, и помогает нам не забыть изменить какую – либо настройку. В нашем демонстрационном проекте необходимо изменить только одну строку в файле конфигурации, которая отвечает за параметры доступа к базе данных.

<?xml version="1.0" encoding="UTF-8"?>
<config>
// "production" секция
<production>
<db>
<dsn>mysql://dbowner:password@localhost/maindb</dsn>
<attributes>
<model_loading>conservative</model_loading>
</attributes>
</db>
</production>
// Тестовая секция, наследует "production" секцию.
<test extends="production">
<db>
<dsn>mysql://dbowner:password@localhost/maindb_test</dsn>
</db>
</test>
</config>

Пр. переводчика: подобный подход с секциями возможен и при использовании Zend_Config на основе .ini файлов.

Обратите внимание на то как мы наследуем дочерние атрибуты: после того как мы создали элемент и на его основе создали новый(по принципу наследования), мы не должны переопределять дочерние свойства, если этого не требуется.

Итак, мы получили наш конфигурационный файл, теперь необходимо что – то, что позволит нам управлять конфигурацией и переключаться между средами выполнения (окружениями). Эту задачу берёт на себя наш Initializer plugin (плагин инициализации) который принимает в качестве параметра своего конструктора среду выполнения. Рассмотрение исходного кода плагина инициализации выходит за рамки нашей статьи, но для любопытных код можно найти по ссылке

Простой пример

Давайте начнём с основ контроллера тестирования. Если вы используете Zend Studio for Eclipse, вы можете легко создать подобную структуру с помощью правой кнопки мыши по вашему контроллеру в PHP Explorer и выбрав New > Zend Framework Item, а затем выбрав Zend Controller Test Case. Далее просто убедитесь что выбран к именно тот контроллер, для которого вы хотите создать тест, и нажмите "finish".

require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';
require_once 'application/Initializer.php';
require_once 'application/default/controllers/IndexController.php';

class AccountControllerTest extends Zend_Test_PHPUnit_ControllerTestCase {

/**
* Выбираем среду выполнения прежде чем запустить тест
*/
protected function setUp() {
$this->bootstrap = array ($this, 'appBootstrap' );
parent::setUp ();
// TODO Auto-generated FooControllerTest::setUp()
}

/**
* Выбираем среду выполнения прежде чем запустить тест
*/
public function appBootstrap() {
$this->frontController->registerPlugin ( new Initializer( 'test' ) ); // Строка 20
}

/**
* Возвращаем исходное состояние среды выполнения после того как мы
* завершили тест
*/
protected function tearDown() {
// TODO Auto-generated FooControllerTest::tearDown()
parent::tearDown ();
}

/**
* Constructs the test case.
*/
public function __construct() {
// TODO Auto-generated constructor
}

/**
* Tests FooController->barAction()
*/
public function testIndexAction() { // Строка 41
// TODO Auto-generated FooControllerTest->testBarAction()
$this->dispatch ( '/index/index' );
$this->assertController ( 'index' );
$this->assertAction ( 'index' );
}
}

На строке 20 видно, что мы используем плагин инициализации, для выбора среды выполнения перед запуском теста. Перед тем как запустить каждый из методов – тестеров, PHPUnit будет вызывать наш setUp() метод, запрограммированный на вызов appBootstrap() метода. Таким образом, мы можем быть уверенными в том, что используем "чистую" конфигурацию и среду выполнения для каждого теста, то есть так же как если бы наши тесты выполнялись в отдельных процессах. После того как каждый наш тест выполнен, вызывается метод tearDown(). Он должен содержать код, закрывающий открытые ресурсы и устраняющий все изменения, внесенные нашим тестом. Мы сделаем это позже в расширенном примере.

На 41ой строке находиться простой тест, который будет гарантировать что в результате обращения к "/index/index" мы действительно вызвали контроллер "index" и действие "index" и они являются последними выполненными. Это довольно тривиальная проверка, но она помогает находить ошибки в контроллерах.

Выполнение тестов

Для того чтобы статья была чётко ориентированной по теме создания тестов на основе Zend Framework, я решил убрать данный абзац из статьи и сконцентрировать наше внимание на написание тестов. Если вы испытываете затруднения при создании набора тестов и в выполнение ваших тестов из командной строки - обратитесь к документации PHPUnit. Обратите внимание на раздел, посвященный созданию набора тестов (Test Suites), и на раздел о выполнении тестов из командной строки.

Расширение функциональных возможностей тестов

Итак, мы освоили азы написания тестов. Теперь покроем тестами наш контроллер Accounts. Есть довольно большой список требований, который необходимо выполнить для тестирования контроллера. В первую очередь нам нужно создать тест, который покроет весь процесс создания учётной записи, то есть так же, как если бы регистрировался реальный пользователь. После того как мы напишем тест, необходимо создать механизм уничтожающий вновь созданные данные, чтобы база данных не увеличивалась в объеме от многократного выполнения тестов. Во-вторых, нам нужно разработать механизм, который будет имитировать аутентификацию пользователя.

Перечисленные операции довольно общие, то есть существует вероятность, что мы будем повторно использовать данный код. Чтобы этого избежать – создадим родительский класс, от которого и будем наследоваться в дальнейшем.

Одноразовые тестовые модели

Есть два варианта, которые позволяют нам быть уверенными в том, что данные созданные во время работы теста были уничтожены после завершения теста. Первый вариант – это создавать базу данных "на лету", то есть перед каждым выполнением теста. Но так как я использую Doctrine для моих проектов и работаю только с моделью (не использую прямых запросов) в тестах, я решил, что простое удаление созданных данных будет лучшим способом достижения цели. Для того чтобы это сделать необходимо отметить нашу модель как "одноразовую", таким образом она будет удалена после создания (или загрузки).

protected function _setDisposable( Doctrine_Record $model )
{
$this->_disposables[] = $model;
}

Эта функция в качестве аргумента принимает ссылку на модель и записывает её в массив, который будет позднее обработан функцией tearDown():

protected function tearDown()
{
parent::tearDown();
foreach ( $this->_disposables as $model ) {
if ( $model instanceof Doctrine_Record ) {
$model->delete();
}
unset( $model );
}
}

Мы проходим по списку "отмеченных" моделей и удаляем их. Это должно быть сделано именно в функции tearDown(), а не в каком либо другом методе, так как это единственный способ быть уверенными в том, что удаление действительно произошло.

В случае если проверка на то, что выполнилось нужное действие или контроллер (с помощью assert* методов) окажется неудачной или произойдёт какое-либо исключение, метод tearDown() прекратит работу теста. Если мы попробуем поместить удаление моделей после этой проверки, то велика вероятность что наши модели никогда не будут удалены, так же мы не можем производить удаление моделей до проверки, так как есть вероятность того, что наши модели будут нужны в запрашиваемом действии.

Поддержка аутентификации

Нам необходимо сделать три вещи, чтобы полностью протестировать аутентификацию.

  • Создать имитационную учётную запись.
  • Сделать так чтобы наша среда выполнения была такой же, как если бы пользователь вошел в систему.
  • Проверить, была ли переключена среда выполнения в режим пользователя, вошедшего в систему

Наш учебный контроллер обращается к базе данных на предмет аутентификации (Пр. автор использует ZendX_Doctrine_Auth_Adapter). Для того чтобы сымитировать вход пользователя в систему - необходимо создать (или загрузить) запись в таблице учётных записей.

/**
* Создаёт учётную запись, необходимую для имитации
* авторизированного пользователя
*
* @return StdClass an identity
*/
protected function _generateFakeIdentity()
{
$identity = new stdClass();

$account = new Account();
$account->username = 'AutoTest' . time();
$account->emailAddress = 'autotest@example.org';
$account->password = md5( 'password' );
$account->confirmed = true;
$account->enabled = true;
$account->save();

$this->_setDisposable( $account );

foreach( $account->toArray() as $key => $val ) {
$identity->$key = $val;
}
unset( $identity->password );

return $identity;
}

Учётная запись это наша модель типа Doctrine_Record. Мы просто создаём произвольный аккаунт в системе и возвращаем его как нашу идентифицирующую сущность (identity). Обратите внимание на то, что мы так же заносим модель в список временных ("одноразовых") объектов. Теперь необходимо сделать, чтобы наша среда выполнения была такой же, как если бы пользователь вошёл в систему.

/**
* Изменяет текущую среду в состояние соответствующее пользователю,
* вошедшему в систему
*
* @param object $identity the idenity to use, otherwise one is generated
* @return void
*/
protected function _doLogin( $identity = null )
{
if ( $identity === null ) {
$identity = $this->_generateFakeIdentity();
}
Zend_Auth::getInstance()->getStorage()->write( $identity );
}

В нашем учебном приложении, пользователь считается авторизированным, если Zend_Auth имеет идентифицирующую сущность, идентификатор (identity). Следовательно, все, что мы должны сделать - это сохранить этот идентификатор в хранилище Zend_Auth и "обозвать" себя "вошедшим в систему". Данная операция так же проста, как проверка на наличие идентификатора.

public function assertNotLoggedIn()
{
$this->assertFalse( Zend_Auth::getInstance()->hasIdentity(), 'Login assertion failed' );
}

public function assertLoggedIn()
{
$this->assertTrue( Zend_Auth::getInstance()->hasIdentity(), 'Login assertion failed' );
}

Собираем всё вместе

Итак, у нас есть родительский класс с необходимой базовой функциональностью для наших тестов.

<?php

require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';

class BaseControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{

/**
* Наш контейнер для моделей, которые должным быть уничтожены после
* выполнения теста
* @var array
*/
protected $_disposables = array();

protected function tearDown()
{
parent::tearDown();

foreach ( $this->_disposables as $model ) {
if ( $model instanceof Doctrine_Record ) {
$model->delete();
}
unset( $model );
}
}

/**
* Помечает нашу модель как временную, для автоматического удаления
* её при завершении теста
*
* @param Doctrine_record $model
*/
protected function _setDisposable( Doctrine_record $model )
{
$this->_disposables[] = $model;
}

/**
* Изменяет текущую среду в состояние соответствующее пользователю,
* вошедшему в систему
*
* @param object $identity the idenity to use, otherwise one is generated
* @return void
*/
protected function _doLogin( $identity = null )
{
if ( $identity === null ) {
$identity = $this->_generateFakeIdentity();
}

Zend_Auth::getInstance()->getStorage()->write( $identity );
}

/**
* Создаёт учётную запись, необходимую для имитации
* авторизированного пользователя
*
* @param boolean $unique
* @return StdClass an identity
*/
protected function _generateFakeIdentity( $unique = false )
{
$identity = new stdClass();

$account = new Account();

$account->username = 'AutoTest' . time();
$account->emailAddress = 'autotest' . time() . '@example.org';
$account->password = md5( 'password' );
$account->confirmed = true;
$account->enabled = true;
$account->save();

$this->_setDisposable( $account );

foreach( $account->toArray() as $key => $val ) {
$identity->$key = $val;
}
unset( $identity->password );

return $identity;
}

public function assertNotLoggedIn()
{
$this->assertFalse( Zend_Auth::getInstance()->hasIdentity(), 'Login assertion failed' );
}

public function assertLoggedIn()
{
$this->assertTrue( Zend_Auth::getInstance()->hasIdentity(), 'Login assertion failed' );
}
}

Написание теста контроллера

Когда наши подготовительные работы были закончены - мы можем приступить к написанию нашего контроллера – теста.

Наша первая задача для теста звучит следующим образом - "после того как пользователь зарегистрировался, он должен подтвердить регистрацию через email". Для этого нам необходимо сымитировать отправку пользователем корректных регистрационных данных нашему контроллеру, и убедиться в том, что созданная учётная запись является не активированной. Так же нам необходимо убедиться в том, что идентификация пользователя не будет успешной в том случае, если пользователь пытается войти под не активированной учётной записью.

public function testRegisterCreatesNewUnconfirmedAccount()
{
$email = 'autotest' . time() . '@example.org';
$data = array(
'emailAddress' => $email,
'password' => 'testpassw0rd',
'passwordconfirm' => 'testpassw0rd'
);

$_POST = $data;

$this->dispatch( '/account/register' );
// Пытаемся найти созданную запись
$table = Doctrine_Table::create( 'Account' ) ;
$account = $table->findOneByEmailAddress( $email );
$this->_setDisposable( $account );
$this->assertNotNull( $account );
$this->assertFalse( $account->confirmed, 'Account was not marked as unconfirmed' );
}

/**
* Проверка на то, что не активированная учётная запись не может пройти
* авторизацию
*
*/
public function testUnconfirmedUserCannotLogin()
{
$email = 'autotest' . time() . '@example.org';

$account = new Account();
$account->username = $email;
$account->password = md5( 'password' );
$account->emailAddress = $email;
$account->confirmed = false;
$account->enabled = true;
$account->save();

$this->_setDisposable( $account );

$_POST['username'] = $email;
$_POST['password'] = 'password';

$this->dispatch( '/account/login' );
$this->assertFalse( Zend_Auth::getInstance()->hasIdentity() );
$this->assertNotRedirect();
}

Наш первый тест использует глобальный массив $_POST для эмуляции отправки данных формой регистрации. После выполнения AccountController::registerAction(), мы с помощью Doctrine_Table проверяем, была ли создана запись (методом поиска), и помечена ли эта запись как не активированная.

Второй тест "вручную" создаёт не активированную запись и "убеждается" в том, что пользователь не может выполнить авторизацию, не активировав учётную запись. В дополнение мы используем assertNotRedirect(), чтобы убедиться? что наш контроллер не перенаправляет пользователя на другую страницу. Ведь контроллер может перенаправлять пользователя только в случае успешной идентификации, в случае же неудачной он должен вывести сообщение ошибки.

Заключение

Как мы выяснили, автоматизировать тестирование контроллеров довольно просто, совмещая возможности PHPUnit и компонента Zend_Test. Мы можем добавить дополнительную функциональность нашим тестам, используя имитацию идентификации и создавая имитационные учётные записи. А так же мы узнали, как очистить базу данных после выполнения тестов. Я показал Вам, как можно собрать это всё воедино для тестирования регистрации и активации учётных записей в контроллерах.

Во второй части статьи я покрою тестами ещё большую часть функциональности нашего AccountsController, включая те действия, доступ к которым может быть произведён только идентифицированным пользователем. Стоит отметить, что если по каким-то причинам метод tearDown() не был вызван, то созданные записи останутся в базе данных. Есть ещё один способ, о котором мы не поговорили – это создание процедуры on_shutdown, но это больше похоже на применение оружия массового поражения.

P.S. это была первая часть из серии статей о автоматизированном тестировании от A.J. Brown. Если перевод будет востребован, по мере появления следующих частей  - я постараюсь также их перевести.

Тема для обсуждения на форуме здесь.

Лучший способ следить за обновлениями сайта это подписаться на RSS
Если информация была полезной для вас, вы можете поддержать сайт.
Комментарии:
Zh0rzh 23.02.2009 21:26 #
На форуме будет тема для обсуждения?
Ответить
Александр Махомет 24.02.2009 00:17 #
Обновил статью, добавил в конце ссылку.
Ответить
maxyc 25.02.2009 13:48 #
одним словом. а зачем это надо? что тестируется?
Ответить
Александр Махомет 26.02.2009 01:00 #
Для того чтобы быть уверенным что программа не перестанет работать после какого либо изменения. Тесты избавляют вас от необходимости самому все проверять руками после правок.
Ответить
ramusus 25.02.2009 15:35 #
Я не нашел ссылку на исходники тестов и модулей авторизации и регистрации. Можно попросить опубликовать их? Было бы интересно взглянуть на подход автора.
Ответить
Александр Махомет 26.02.2009 00:58 #
Обратитесь к автору, возможно он опубликует.
Ответить
Oleg Lobach 05.04.2009 20:45 #
А вторую часть собираетесь переводить?
Ответить
Александр Махомет 05.04.2009 20:54 #
Олег, а она разве вышла?
Ответить
lcf 05.04.2009 21:09 #
Вроде он уже какую-то вторую часть уже переводит.
Ответить
Александр Махомет 05.04.2009 21:14 #
Рашад переводит про ACL :)
Ответить
lcf 05.04.2009 21:19 #
А чорт, попутал тему, оно же все в одну ленту сыпется :)

Прошу прощения.
Ответить
Уважаемые пользователи. Комментарии не для того чтобы:
  1. Спрашивать почему у вас не работает код, для этого есть тема форума закрепленная за статьей.
  2. Спрашивать как реализовать ту или иную функциональность, для этого необходимо создать свою тему на форуме.

Комментарии для того чтобы: высказать свое аргументированное мнение о статье, указать какие участки вызывают непонимание, что нужно исправить/улучшить, просто сказать спасибо.

Комментарии имеют древовидную структуру.
Если вы хотите ответить на определенный комментарий - нажмите на ссылку "Ответить" возле этого комментария.

Комментарии не соответствующие этим правилам могут быть удалены. Спасибо за понимание.
Комментарии временно отключены, вы можете воспользоваться форумом.