Dezembro 17th 2007 08:48 pm

Integrando PHPUnit com banco de dados

A partir da versão 3.2.0 o PHPUnit conta com uma extensão para fazer testes utilizando banco de dados, na pratica isto já era possível, mas o PHPUnit não contava com nenhum tipo de ferramenta para abstrair tarefas como: inserir dados para fazer testes, deletar dados apos as rotinas de testes, etc.

O fato e que esta extensão do PHPUnit é uma forma elegante de controlar a dependência entre o banco e sua aplicação.

Obs.: Aos que ainda não estão familiarizados com a utilização do PHPUnit recomendo a leitura destes posts:
http://blog.diegotremper.com/archives/15
http://blog.diegotremper.com/archives/97

Exemplo:
Vamos supor que você queira testar um método de sua aplicação que carregue um determinado registro do banco de dados. A questão é: como garantir que quando eu estiver rodando meu teste, o registro que eu vou tentar carregar para afirmar que meu método esta funcionando corretamente, estará lá?

Como você já deve ter pensado, primeiro teremos que inserir o registro na base de dados antes de executarmos nosso teste, e depois da execução, teremos que deletar o registro para que a base de dados não fique com registros inválidos.


Utilizare a classe CadastroFuncionarios para criar os exemplos de teste, abaixo o seu código:

PHP:
  1. <?php
  2. require_once(dirname(__FILE__).'/Funcionario.php');
  3.  
  4. class CadastroFuncionarios
  5. {
  6.  
  7.     public function load($cpf)
  8.     {
  9.         $functionario = null;
  10.         $db = new PDO('mysql:host=localhost;dbname=dbunit', 'root', '');
  11.         $stmt = $db->prepare('SELECT * FROM funcionario WHERE cpf=:cpf');
  12.         $stmt->bindParam(':cpf', $cpf);
  13.         $stmt->execute();
  14.  
  15.         $data = $stmt->fetchAll();
  16.        
  17.         if (count($data) == 1) {
  18.             $functionario = new Funcionario();
  19.             $functionario->cpf = $data[0]['cpf'];
  20.             $functionario->nome = $data[0]['nome'];
  21.         }
  22.                        
  23.         return $functionario;
  24.     }
  25. }

A classe possui apenas um método (load), o qual tem a responsabilidade de retornar um funcionário do banco de dados baseado no CPF passado como parâmetro.

Abaixo a estrutura da tabela do banco de dados que iremos utilizar.


CREATE TABLE `funcionario` (
`cpf` varchar(11) NOT NULL,
`nome` varchar(70) default NULL,
PRIMARY KEY (`cpf`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into `funcionario`(`cpf`,`nome`) values ('22222222222','Toquinho');

Antes da verão 3.2.0 do PHPUnit poderiamos fazer nosso teste da seguinte forma:

PHP:
  1. <?php
  2.  
  3.  
  4. require_once (dirname ( dirname ( dirname ( __FILE__ ) ) ) . '/Application/Funcionario/CadastroFuncionarios.php') ;
  5.  
  6. require_once('PHPUnit/Framework/TestCase.php');
  7.  
  8.  
  9. /**
  10.  * CadastroFuncionarios test case.
  11.  */
  12. class CadastroFuncionariosTest extends PHPUnit_Framework_TestCase
  13. {
  14.  
  15.     /**
  16.      * @var CadastroFuncionarios objeto de testes
  17.      */
  18.     private $CadastroFuncionarios;
  19.    
  20.     /**
  21.      * Cria o ambiente de execução do teste
  22.      */
  23.     protected function setUp()
  24.     {
  25.         parent::setUp();
  26.         $this->CadastroFuncionarios = new CadastroFuncionarios();
  27.         $this->pdo = new PDO ( 'mysql:host=localhost;dbname=dbunit', 'root', 'admin' ) ;
  28.         $this->pdo->query("INSERT INTO funcionario VALUES ('99999999999', 'João')");
  29.     }
  30.  
  31.     /**
  32.      * Limpa os registros inseridos após a execução do teste
  33.      */
  34.     protected function tearDown()
  35.     {
  36.         $this->CadastroFuncionarios = null;
  37.         $this->pdo->query("DELETE FROM funcionario WHERE cpf = '99999999999'");
  38.         parent::tearDown();
  39.     }
  40.  
  41.    
  42.     /**
  43.      * Testa CadastroFuncionarios->Load()
  44.      */
  45.     public function testLoad()
  46.     {
  47.         $this->assertEquals ( $this->CadastroFuncionarios->load ( '99999999999' )->nome, 'João' ) ;
  48.     }
  49.    
  50. }

Explicando:

  • CadastroFuncionariosTest::setUp(): cria uma conexão com a base de dados e insere o registro que utilizaremos para nosso teste
  • CadastroFuncionariosTest::testLoad(): executa o método CadastroFuncionarios::load() passando o cpf do funcionário que acabamos de inserir no setUp
  • CadastroFuncionariosTest::tearDown(): limpa o registro que inserimos na base de dados como teste

Esta é uma forma simples de testarmos nossa classe, mas esta forma começa a se tornar "chata" quando temos muitos registros para serem inseridos e deletados, quando precisamos gravar dados em mais de uma tabela, etc.

Similar ao DBUnit, a extensão do PHPUnit permite ao usuário configurar os registros que seram inseridos na base de dados através de um arquivo no formato xml, para os testes que fiz, utlizei o seguinte arquivo.

XML:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <dataset>
  3.   <funcionario cpf="88888888888" nome="Maria" />
  4.   <funcionario cpf="66666666666" nome="Antonio" />
  5.   <funcionario cpf="44444444444" nome="Eva" />
  6.   <funcionario cpf="33333333333" nome="Gabriel" />
  7.   <funcionario cpf="11111111111" nome="Judas" />
  8.   <funcionario cpf="00000000000" nome="Buda" />
  9. </dataset>

A estrutura do arquivo é muito simples:

XML:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <dataset>
  3.   <nomeDaTabela campo="valor" campo="valor" />
  4. </dataset>

Agora vamos ver como ficaria nossa classe de testes utilizando a estensão do PHPUnit:

PHP:
  1. <?php
  2.  
  3. require_once ('PHPUnit/Extensions/Database/TestCase.php') ;
  4. require_once 'PHPUnit/Extensions/Database/Operation/DeleteAll.php';
  5. require_once 'PHPUnit/Extensions/Database/Operation/Delete.php';
  6.  
  7. require_once (dirname ( dirname ( dirname ( __FILE__ ) ) ) . '/Application/Funcionario/CadastroFuncionarios.php') ;
  8.  
  9. /**
  10.  * CadastroFuncionarios test case.
  11.  */
  12. class CadastroFuncionariosTest extends PHPUnit_Extensions_Database_TestCase {
  13.    
  14.     /**
  15.      * @var PDO
  16.      */
  17.     private $pdo ;
  18.    
  19.     /**
  20.      * @var CadastroFuncionarios  objeto de testes
  21.      */
  22.     private $CadastroFuncionarios ;
  23.    
  24.     protected function getSetUpOperation() {
  25.         return PHPUnit_Extensions_Database_Operation_Factory::INSERT();
  26.     }
  27.    
  28.         protected function getTearDownOperation()
  29.         {
  30.             return PHPUnit_Extensions_Database_Operation_Factory::DELETE();
  31.         }
  32.    
  33.     protected function getConnection () {
  34.         if ($this->pdo == null)
  35.             $this->pdo = new PDO ( 'mysql:host=localhost;dbname=dbunit', 'root', 'admin' ) ;
  36.        
  37.         return $this->createDefaultDBConnection ( $this->pdo, 'dbunit' ) ;
  38.     }
  39.    
  40.     protected function getDataSet () {
  41.         return $this->createFlatXMLDataSet ( dirname ( __FILE__ ) . '/resource/funcionarios.xml' ) ;
  42.     }
  43.    
  44.     protected function setUp () {
  45.         parent::setUp () ;
  46.         $this->CadastroFuncionarios = new CadastroFuncionarios ( ) ;
  47.     }
  48.    
  49.     protected function tearDown () {
  50.         $this->CadastroFuncionarios = null ;
  51.         parent::tearDown () ;
  52.     }
  53.    
  54.     /**
  55.      * Testa CadastroFuncionarios->Load()
  56.      */
  57.     public function testLoad () {
  58.         $this->assertEquals ( $this->CadastroFuncionarios->load ( '88888888888' )->nome, 'Maria' ) ;
  59.     }
  60. }

Por partes:

Um pouco diferente do método convencional com que criamos nossas classes de testes (estendendo a classe PHPUnit_Framework_TestCase), devemos estender a classe PHPUnit_Extensions_Database_TestCase a qual nos dá alguns métodos para facilitar nosso trabalho.

Para os métodos getConnection() e getDataSet() que implementei na classe de testes, são dois métodos abstratos herdados da classe PHPUnit_Extensions_Database_TestCase, o getConnection deve reportar uma instância da interface PHPUnit_Extensions_Database_DataSet_IDataSet is e o método getDataSet deve retornar uma instância da interface PHPUnit_Extensions_Database_DB_IDatabaseConnection, estes dois objetos servem para o PHPUnit manipular a conexão com a base de dados e criar os registros no banco de dados a partir do xml de configuração, respectivamente.

Por baixo dos panos, para cada execução de teste do PHPUnit, os métodos setUp e tearDown são executados antes e depois de cada execução, respectivamente. Para pegar a conexão com o banco de dados o framework utiliza o método getConnection e para pegar o dataset de registros é utilizado o método getDataSet.

Ok, mas e os métodos getSetUpOperation e getTearDownOperation, para que servem?

Estes métodos definem quais seram as operações feitas sobre o dataset (arquivo xml) antes e depois de cada teste. Por padrão, as operações executadas antes e depois de cada método de teste são: PHPUnit_Extensions_Database_Operation_Factory::CLEAN_INSERT(); e PHPUnit_Extensions_Database_Operation_Factory::NONE();. O que significa, que em nosso exemplo acima, o fluxo de nosso teste ficaria da seguinte forma.

  • CadastroFuncionariosTest::setUp();: método que o phpunit sempre executa antes de cada teste
  • CadastroFuncionariosTest::getConnection();: pega a conexão com o banco de dados
  • CadastroFuncionariosTest::getSetUpOperation();: pega qual será a operação realizado sobre o dataset
  • CadastroFuncionariosTest::getDataSet();: pega o dataset configurado e aplica a operação de setUp (no nosso caso INSERT)
  • CadastroFuncionariosTest::testLoad();: executa o teste
  • CadastroFuncionariosTest::tearDown();: método executado depois de cada teste
  • CadastroFuncionariosTest::getTearDownOperation();: pega a operação a ser executada após o teste
  • CadastroFuncionariosTest::getDataSet();: aplica a operação sobre o dataset (em nosso exemplo DELETE)

Caso não tivessemos declarado os métodos getSetUpOperation e getTearDownOperation o PHPUnit iria trucar todos os dados que estivessem em nossa tabela, inserir os dados descritos em nosso arquivo xml e após o teste, iria deletar os dados inseridos. Como declaramos as operações INSERT e DELETE, antes do testes, ao invés de trucar todos os dados já existentes na tabela, o PHPUnit apenas colocara os dados de nosso arquivo xml, assim conservando os dados já existentes anteriormente ná tabela. Ainda existem como opções as operações:

NONE: Não faz nada (isto mesmo nada!).
TRUNCATE: Deleta todos os dados da tabela;
DELETE_ALL: Exatamente a mesma coisa que a anterior.
UPDATE: Ao invés de inserir elá atualiza os registros, baseado na chave primária de sua tabela.

Uma obeservação importante:
Reparem que nas primeiras linhas do arquivo de testes que coloquei no exemplo, existem dois require_once.

PHP:
  1. require_once 'PHPUnit/Extensions/Database/Operation/DeleteAll.php';
  2. require_once 'PHPUnit/Extensions/Database/Operation/Delete.php';

estas duas linhas são para corrigir um bug que tive a oportunidade de descobrir no PHPUnit :S, a correção estará disponivel na release 3.2.7, mas já está disponivel no repositório do projeto. Quem quiser dar uma olhadinha no bug http://www.phpunit.de/ticket/314.

Os todos os arquivos utilizados neste post, estão dispoíveis neste link.

Espero ter sido claro a todos, mas em caso de dúvidas é só postar ai.
Abraço a todos.

Compartilhe:
  • del.icio.us
  • Google
  • Digg
  • Sphinn
  • Facebook
  • Mixx
  • LinkedIn
  • Live
  • Rec6
  • Technorati
  • TwitThis
1 Estrela2 Estrela3 Estrela4 Estrela5 Estrela (Nenhuma avaliação ainda)
Loading ... Loading ...

4 Comments »

4 Responses to “Integrando PHPUnit com banco de dados”

  1. battisti on 17 Jan 2008 at 14:04 #

    Saudações

    Muito bom o texto. Tenho uma dúvida. Quando eu tiver uma tabela com chaves estrangeiras como fica o XML?

    []’s Anselmo Battisti

  2. Diego Tremper on 18 Jan 2008 at 22:42 #

    Ola Battisti,

    ex.: vamos supor que você tenha uma tabela chamada “cargo” com os campos “id” e “nome” e em sua tabela tenha uma FK “id_cargo” que aponta para “cargo.id”, você pode fazer da seguinte forma:

    < ?xml version="1.0" encoding="UTF-8"?>
    < dataset >
    < cargo id="1" nome="Programador" />
    < cargo id="2" nome="Gerente" />
    < cargo id="3" nome="Analista" />

    < funcionario cpf="88888888888" nome="Maria" id_cargo="1" />
    < funcionario cpf="66666666666" nome="Antonio" id_cargo="1" />
    < funcionario cpf="44444444444" nome="Eva" id_cargo="3" />
    < funcionario cpf="33333333333" nome="Gabriel" id_cargo="1" />
    < funcionario cpf="11111111111" nome="Judas" id_cargo="3" />
    < funcionario cpf="00000000000" nome="Buda" id_cargo="2" />
    < /dataset>

    Abraço

  3. Diego Tremper on 19 Jan 2008 at 01:19 #

    Ola Battisti,

    no exemplo acima percebi que a execução do teste dará um erro, porque ao final da execução dos testes o PHPUnit tentará primeiro deletar os registros da tabela cargo, gerando um erro de integridade caso exista uma FK, já fiz a correção para isso em minha máquina, vou abrir o tickek no site do PHPUnit e postar a correção.

    Abraço

  4. Diego Tremper on 19 Jan 2008 at 01:43 #

    Ok Battisti,

    já postei o defeito e correção para o bug no site do PHPUnit, caso queira acompanhar segue o link http://www.phpunit.de/ticket/354

    Abraço

Trackback URI | Comments RSS

Leave a Reply

« array X ArrayIterator == cópia X referência | Busca no site? Com Zend_Search_Lucene claro! »