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
-
require_once(dirname(__FILE__).'/Funcionario.php');
-
-
class CadastroFuncionarios
-
{
-
-
public function load($cpf)
-
{
-
$functionario = null;
-
$db = new PDO('mysql:host=localhost;dbname=dbunit', 'root', '');
-
$stmt = $db->prepare('SELECT * FROM funcionario WHERE cpf=:cpf');
-
$stmt->bindParam(':cpf', $cpf);
-
$stmt->execute();
-
-
$data = $stmt->fetchAll();
-
-
if (count($data) == 1) {
-
$functionario = new Funcionario();
-
$functionario->cpf = $data[0]['cpf'];
-
$functionario->nome = $data[0]['nome'];
-
}
-
-
return $functionario;
-
}
-
}
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
-
-
-
require_once (dirname ( dirname ( dirname ( __FILE__ ) ) ) . '/Application/Funcionario/CadastroFuncionarios.php') ;
-
-
require_once('PHPUnit/Framework/TestCase.php');
-
-
-
/**
-
* CadastroFuncionarios test case.
-
*/
-
class CadastroFuncionariosTest extends PHPUnit_Framework_TestCase
-
{
-
-
/**
-
* @var CadastroFuncionarios objeto de testes
-
*/
-
private $CadastroFuncionarios;
-
-
/**
-
* Cria o ambiente de execução do teste
-
*/
-
protected function setUp()
-
{
-
parent::setUp();
-
$this->CadastroFuncionarios = new CadastroFuncionarios();
-
$this->pdo = new PDO ( 'mysql:host=localhost;dbname=dbunit', 'root', 'admin' ) ;
-
$this->pdo->query("INSERT INTO funcionario VALUES ('99999999999', 'João')");
-
}
-
-
/**
-
* Limpa os registros inseridos após a execução do teste
-
*/
-
protected function tearDown()
-
{
-
$this->CadastroFuncionarios = null;
-
$this->pdo->query("DELETE FROM funcionario WHERE cpf = '99999999999'");
-
parent::tearDown();
-
}
-
-
-
/**
-
* Testa CadastroFuncionarios->Load()
-
*/
-
public function testLoad()
-
{
-
$this->assertEquals ( $this->CadastroFuncionarios->load ( '99999999999' )->nome, 'João' ) ;
-
}
-
-
}
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 version="1.0" encoding="UTF-8"?>
-
<dataset>
-
<funcionario cpf="88888888888" nome="Maria" />
-
<funcionario cpf="66666666666" nome="Antonio" />
-
<funcionario cpf="44444444444" nome="Eva" />
-
<funcionario cpf="33333333333" nome="Gabriel" />
-
<funcionario cpf="11111111111" nome="Judas" />
-
<funcionario cpf="00000000000" nome="Buda" />
-
</dataset>
A estrutura do arquivo é muito simples:
-
<?xml version="1.0" encoding="UTF-8"?>
-
<dataset>
-
<nomeDaTabela campo="valor" campo="valor" />
-
</dataset>
Agora vamos ver como ficaria nossa classe de testes utilizando a estensão do PHPUnit:
-
<?php
-
-
require_once ('PHPUnit/Extensions/Database/TestCase.php') ;
-
require_once 'PHPUnit/Extensions/Database/Operation/DeleteAll.php';
-
require_once 'PHPUnit/Extensions/Database/Operation/Delete.php';
-
-
require_once (dirname ( dirname ( dirname ( __FILE__ ) ) ) . '/Application/Funcionario/CadastroFuncionarios.php') ;
-
-
/**
-
* CadastroFuncionarios test case.
-
*/
-
class CadastroFuncionariosTest extends PHPUnit_Extensions_Database_TestCase {
-
-
/**
-
* @var PDO
-
*/
-
private $pdo ;
-
-
/**
-
* @var CadastroFuncionarios objeto de testes
-
*/
-
private $CadastroFuncionarios ;
-
-
protected function getSetUpOperation() {
-
return PHPUnit_Extensions_Database_Operation_Factory::INSERT();
-
}
-
-
protected function getTearDownOperation()
-
{
-
return PHPUnit_Extensions_Database_Operation_Factory::DELETE();
-
}
-
-
protected function getConnection () {
-
if ($this->pdo == null)
-
$this->pdo = new PDO ( 'mysql:host=localhost;dbname=dbunit', 'root', 'admin' ) ;
-
-
return $this->createDefaultDBConnection ( $this->pdo, 'dbunit' ) ;
-
}
-
-
protected function getDataSet () {
-
return $this->createFlatXMLDataSet ( dirname ( __FILE__ ) . '/resource/funcionarios.xml' ) ;
-
}
-
-
protected function setUp () {
-
parent::setUp () ;
-
$this->CadastroFuncionarios = new CadastroFuncionarios ( ) ;
-
}
-
-
protected function tearDown () {
-
$this->CadastroFuncionarios = null ;
-
parent::tearDown () ;
-
}
-
-
/**
-
* Testa CadastroFuncionarios->Load()
-
*/
-
public function testLoad () {
-
$this->assertEquals ( $this->CadastroFuncionarios->load ( '88888888888' )->nome, 'Maria' ) ;
-
}
-
}
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.
-
require_once 'PHPUnit/Extensions/Database/Operation/DeleteAll.php';
-
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.
4 Comments »














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
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
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
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