Automatizar os testes, em suas aplicações web, é um importante passo no sentido de ter a confiança necessária para realizar as alterações em sua aplicação e manter a mesma confiança para entregar um produto com qualidade e livre de problemas. Com o Zend Framework gerenciando seus testes (através do PHPUnit), você pode construir um conjunto muito bom de “caso de testes” para sua aplicação sem muito esforço.
A “Parte 1″, dará para vocês todas as informações básicas, que são necessárias para começar a escrever seus próprios testes em suas aplicações que utilizam o Zend Framework. Na parte 2, irei fornecer para vocês alguns exemplos de uso real, sobre outras formas de se escrever testes.
Vamos direto para o assunto.
Para o exemplo abaixo, eu vou utilizar um controller real de um dos meus projetos. Esse Controller manipula atividades nas contas, como por exemplo: logins, logouts, registros e confirmações. Nós vamos usar um banco de dados de teste que é um clone da nossa base de produção e o “Doctrine” para gerenciar o ORM (desculpe-me Zend_Db:()). Eu vou assumir que você está usando o Zend Framework (1.6+) como layout do projeto e que esta um pouco familiarizado com o Zend_Config e usando um plugin de inicialização (que é criado por padrão, no Zend Studio for Eclipse 6.1).
Preparando a sua aplicação
O primeiro passo para configurar a automatização de teste é preparar o ambiente de sua aplicação e configurá-lo corretamente. Dependendo de suas configurações, isso pode envolver setar variáveis globais, trocar a conexão do seu banco de dados ou reconfigurar o path do sistema. Felizmente, tudo isso é facilmente modificado se você usar o Zend_Config e um plugin de inicialização
O Zend_Config permite que você especifique seções e cada uma delas podem herdar dados de outra seção. Isso lhe permite criar configurações para diferentes ambientes sem duplicar opções entre arquivos diferentes (e assim, nos ajudar a garantir que não estamos esquecendo de nada!). No nosso projeto de exemplo, nós vamos precisar modificar nossa string de conexão ao banco, pois estamos utilizando uma base de teste.
< ?xml version="1.0" encoding="UTF-8"?> <config> <production> <db> <dsn>mysql://dbowner:password@localhost/maindb</dsn> <attributes> <model_loading>conservative</model_loading> </attributes> </db> </production> <test extends="production"> <db> <dsn>mysql://dbowner:password@localhost/maindb_test</dsn> </db> </test> </config>
Perceba como nós podemos ainda herdar atributos do “filho”. Mesmo que nós especifiquemos um nó, não precisamos fazer isso com tudo abaixo dele.
Agora que nossa configuração esta OK, precisamos de alguma coisa para gerenciar essa configuração para nós, que ligue e desligue de acordo com o ambiente que estamos executando. E é exatamente para isso que temos um plugin de inicialização, ele aceita o ambiente como parâmetro do construtor para fazer toda esse gerenciamento. Está fora do escopo desse artigo mostrar os fontes desse plugin. Mas aqui tem uma cópia, para os curiosos.
Um exemplo básico
Vamos iniciar com um simples framework controlador de testes. Se você está utilizando o Zend Studio for Eclipse, você pode facilmente criar essa estrutura, clicando com o botão direito do mouse em seu controller no PHP explorer e selecionar New>Zend FrameworkItem e depois, selecione: Zend Controller Teste Case. Então, simplesmente tenha certeza que esse é o controle que você deseja testar e escolha, clicando em 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 {
/**
* Prepares the environment before running a test.
*/
protected function setUp() {
$this->bootstrap = array ($this, 'appBootstrap' );
parent::setUp ();
// TODO Auto-generated FooControllerTest::setUp()
}
/**
* Prepares the environment before running a test.
*/
public function appBootstrap() {
$this->frontController->registerPlugin ( new Initializer( 'test' ) );
}
/**
* Cleans up the environment after running a test.
*/
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() {
// TODO Auto-generated FooControllerTest->testBarAction()
$this->dispatch ( '/index/index' );
$this->assertController ( 'index' );
$this->assertAction ( 'index' );
}
}
Você verá na linha 20, que estamos usando nosso plugin de inicialização, para configurar o ambiente antes de rodar os testes. Antes de cada método de teste ser executado, o PHPUnit irá chamar nosso método setup(), que foi programado para chamar o método appBootstrap. Isso nos assegura que estamos usando uma configuração e um ambiente limpo antes de cada teste, tratando cada um deles como um processo diferente. Após concluir cada teste, o método tearDown() é chamado. Ele é o lugar aonde você deve colocar os códigos responsáveis em remover recursos e resetar as alterações persistentes que cada teste pode ter feito. Nós vamos fazer uso dele nos exemplos mais avançados, que veremos depois.
A linha 41 contém um teste de caso “crú”, que será encarregado de enviar para “/index/index” o resultado do controller nomeado “index” e que a ação “index” será a ultima a ser executada. Isto pode parecer trivial, mas ajuda a detectar erros com seus controladores. Se uma exceção acontecer, o controller assertion irá falhar, a não ser que este seja o ultimo a ser executado.
Executando os seus testes
Para manter esse artigo focado, eu decidi por remover essa seção e continuar escrevendo os testes. Se você precisar de ajuda para criar a suíte de testes e executá-los por linha de comando, de uma olhada na Documentação do PHPUnit especificamente na seção the Command Line Test Runner e Organizing Test Suites. Se você tiver alguma questão específica, sinta-se livre para me mandar um email.
Extendendo a funcionalidade dos testes
Agora que já cobrimos o básico, vamos testar completamente o nosso controller de contas. Existe uma série de requisitos “ocultos”, que nós precisamos conhecer, para testar o controller de contas. Primeiro, nós precisamos de uma maneira de testar o processo completo de registro do usuário, como se fosse ele mesmo fazendo isso. Quando terminarmos com um teste, temos que nos livrar dos dados deles sempre que possivel, assim podemos rodar os testes muitas vezes sem nos preocuparmos em popular demais a nossa base de dados. Segundo, precisamos de uma forma de simular um usuário autenticado, bem como, verificar se um usuário foi autenticado ou não.
Modelos de testes descartáveis
Existem duas formas de garantir que os dados, que você está criando durante os testes, serão deletados assim que ele for concluído. Uma escolha é criar a base de dados durante o processo, utilizando informações iniciais. Desde que eu passei a usar o Doctrine nos meus projetos e trabalhar direto com modelo (sem querys cruas) nos testes, eu decidi que deletar os dados é a melhor solução. Para fazer isso, tudo que você precisa fazer é “agendar” nosso modelo para ser deletado após ele ser criado (ou carregado).
protected function _setDisposable( Doctrine_Record $model )
{
$this->_disposables[] = $model;
}
Essa função, simplesmente pega a referência do modelo e a guarda em um array, que será utilizado pelo método tearDown() mais tarde:
protected function tearDown()
{
parent::tearDown();
foreach ( $this->_disposables as $model ) {
if ( $model instanceof Doctrine_Record ) {
$model->delete();
}
unset( $model );
}
}
Nós simplesmente fazemos um loop, que passa por todos os modelos “agendados” e os deletamos. Isso deve ser feito no tearDown() e não dentro de algum método de testes, porque é a única forma de garantir que aconteça. Uma vez que o assertion falhar ou uma exceção inesperada acontecer durante um teste, aquele método para de executar. SE nós tentarmos destruir nossos modelos no assertion, ele pode nunca vir a acontecer¹. Assim como, se o modelo é necessário para os testes, é óbvio, que não podemos descarta-los antes do assertion (e porque ele vai existir se não precisamos dele?).
Suporte à autenticação
Existem 3 coisas que precisamos saber para podermos fazer um teste completo de autenticação.
- Criar uma identidade falsa.
- Setar nosso ambiente para um estado equivalente de um usuário logado.
- Afirmar se um ambiente foi alterado para um estado de “logado”.
Nosso controller de exemplo usa um adaptador de banco de dados para autenticação e para recuperar uma identidade², então, gerando uma identidade falsa para nós, significa criar (ou carregar) um registro em nossa tabela de contas(usuários) e retornar os dados da identidade que nós normalmente pegaríamos.
/**
* Generates a fake identity, usefull for simulating a logged in user
*
* @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;
}
Account é o nosso modelo (model), um tipo Doctrine_Record. Nós apenas criamos uma conta aleatória e retornamos seus dados como nossa identidade. Note que nós agendamos esse modelo para descarta-lo (Como comentamos a pouco). Agora, nós precisamos apenas de uma maneira de setar nosso ambiente com o estado de “logado” com esse usuário falso.
/**
* Sets the current state as if there is a logged in user
*
* @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 );
}
Nessa nossa aplicação de exemplo, se o Zend_Auth tem uma identidade, é porque um usuário está logado. Então, tudo que precisamos fazer é guardar essa identidade no adaptador do Zend_auth e nos tornar “logados”. Isso faz a afirmação de login simplesmente validar a identidade.
public function assertNotLoggedIn()
{
$this->assertFalse( Zend_Auth::getInstance()->hasIdentity(), 'Login assertion failed' );
}
public function assertLoggedIn()
{
$this->assertTrue( Zend_Auth::getInstance()->hasIdentity(), 'Login assertion failed' );
}
Essas simples afirmações³ garantem se estamos ou não, logados.
Juntando tudo
Juntando tudo, Agora temos uma classe base que irá nos fornecer todos os casos de testes, com as funcionalidades que precisamos.
< ?php
require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';
class BaseControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
/**
* Contains models which should be destroyed on tear down
*
* @var array
*/
protected $_disposables = array();
protected function tearDown()
{
parent::tearDown();
foreach ( $this->_disposables as $model ) {
if ( $model instanceof Doctrine_Record ) {
$model->delete();
}
unset( $model );
}
}
/**
* Sets a model as disposable, so teardown automatically deletes it
*
* @param Doctrine_record $model
*/
protected function _setDisposable( Doctrine_record $model )
{
$this->_disposables[] = $model;
}
/**
* Sets the current state as if there is a logged in user
*
* @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 );
}
/**
* Generates a fake identity, usefull for simulating a logged in user
*
* @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' );
}
}
Escrevendo nosso controller de teste
Agora que a base está funcional, finalmente poderemos iniciar a escrever nossos testes.
Nosso primeiro set de testes cobre os requisitos “quando um usuário se registrar, ele precisa confirmar o seu endereço de email antes de acessar sua conta”. Para fazer isso, nós precisamos simular um usuário postando seus detalhes válidos de registro para o controller e verificar que essa conta não está setada como confirmada. Nós então precisamos submeter as informações de login para uma conta “não confirmada” e garantir que esse usuário não será autenticado.
public function testRegisterCreatesNewUnconfirmedAccount()
{
$email = 'autotest' . time() . '@example.org';
$data = array(
'emailAddress' => $email,
'password' => 'testpassw0rd',
'passwordconfirm' => 'testpassw0rd'
);
$_POST = $data;
$this->dispatch( '/account/register' );
//try to find the account record
$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' );
}
/**
* Asserts that a user that hasn't been confirmed cannot login
*
*/
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();
}
Nosso primeiro teste usa a variável global $_POST, para simular o envio do form de registro, com alguns dados de teste. Após o dispatch(), nós usamos o Doctrine_Table para achar o modelo criado pelo AccountController::registerAction(), e então validar se o registro foi achado e se ele não esta marcado como confirmado.
O segundo teste trabalha inserindo manualmente um registro não confirmado e dando certeza que o usuário não estará autorizado quando ele tentar logar com aquela conta. Como um bonus, nós também usamos assertNotRedirect() para ter certeza que nosso controller não irá redirecionar. Nosso controller deve apenas redirecionar nos casos que o login tenha sido efetuado com sucesso, caso contrário, isso poderia confundir o usuário.
Conclusão
Automatizar testes dos seus controllers é relativamente simples, usando a combinação poderosa do PHPUnit com o componente Zend_test do Zend Framework. Nós podemos adicionar funcionalidades para permitir que nossos testes simule autenticações, crie identidades falsas e sempre limpe nossa base de dados antes de executar um outro teste. Eu mostrei para vocês como colocar tudo isso junto para testar o processo do registro e da confirmação nos seus controllers.
Na parte 2, Eu vou cobrir mais areas de testes no nosso AccountsController, incluindo testes em ações (Actions) que necessitam de usuários autorizados para acessá-las.
- Claro que, se o tearDown() nunca rodar por alguma razão, os modelos não serão deletados também. Mas nos temos mais controle sobre isso. Uma terceira forma, que não foi discutida, é criar uma procedure “on_shutdown”, mas isso não é muito interessante.
- Eu atualemnte uso o ZendX_Doctrine_Auth_Adapter da pasta extras.
- Deixando isso para depois, nós deveriamos, criar um novo critério para a PHPUnit ao invés de usar o assertTrue(), para que nossas mensagens de erros sejam diferentes. Mas é algo para o futuro.
Autor: A.J. Brown
Post Original: http://ajbrown.org/blog/2009/01/04/automated-testing-using-zend-framework-part-1.html
Traduzido Por: Nivaldo Arruda
Posts relacionados:
- Integrando o Zend Framework com o Dojo – Parte III Dando continuidade a nossa integração, vamos dar uma rapida olhada...
- Zend Framework 1.9.0 Baixe o seu em: http://framework.zend.com/download/latest Fora os mais de 700...
- Integrando o Zend Framework com o Dojo Bom, como me bati um pouco para achar boas informações...
Meu sonho um dia foi na empresa que eu trabalhava anteriormente ver tudo coberto “100%” por testes.
E a única coisa que cobrimos por teste, não foi mantida. hahahaha… Mas utopia em um lugar, pode não ser em outro. (;
Acho legal a ImproveIt nesse ponto. d:
Muito bom cara…
Deixa te dar uma idéia, que tal fazer um tutorial ai sobre Decorators? Eu já li várias coisas sobre o assunto, mas nunca achei nada definitivo, que mostrasse com ênfase a capacidade dos Decorators, mostrando como cria-los, utiliza-los. Até mesmo a parte do CSS, tratamento de erros no Zend_Form, utilização de Ajax para exibir os erros, etc…
Obrigado pela vista ao meu blog. Sempre visito o seu.
Ótimo trabalho como sempre.
Abraços.
[...] a sugestão do XAngel irei providenciar um artigo bem completo sobre decorators do zend framework. Mas enquanto [...]
Adorei ver um artigo bem escrito sobre testes no Zend Framework! Estou ansioso pela continuação! Muito bom!
[...] http://www.nivaldoarruda.com.br/2009/02/19/testes-automatizados-com-zend-framework-parte-1/ (Total: [...]
Hey
Estou usando o phpunit e tow criando um arquivo .xml de configuração na tentativa de declarar $_POST como variável global mas sem sucesso (isto é, como o codigo será testado pelo phpunit, sem acesso de um browser to tentando usar esse recurso do arquivo de configuração pra tentar emular o post). Podes me dizer como fazer isto?
Grato
Olá Eder. O que você pode fazer é criar um array falso que se passe pelo $_POST. Na chave do array você usa os mesmos nomes dos campos do formulário e no value os seus valores. Com essa variável emulada você passara para a sua função (via parametro ou global mesmo derrepente);
Abraços
Minha duvida é a seguinte.
Como recupero os dados que o codigo abaixo guardou?
Zend_Auth::getInstance()->getStorage()->write($data);
Quero recupera-los para usar posteriormente, mas não sei como acessar os dados que foram armazenados na sessão.
Bom dia Renato! Você pode tentar acessar os seus dados dessa forma:
$dados = Zend_Auth::getInstance()->getStorage()->read();
ou (jeito mais feinho)
$dados = new Zend_Session_Namespace(‘Zend_Auth’);
echo $dados->storage->SEUATRIBUTO;
Abraços