Capítulo 6. Testando Bancos de Dados

Muitos exemplos de testes unitários iniciantes e intermediários em qualquer linguagem de programação sugerem que é perfeitamente fácil testar a lógica de sua aplicação com testes simples. Para aplicações centradas em bancos de dados isso está longe da realidade. Comece a usar Wordpress, TYPO3 ou Symfony com Doctrine ou Propel, por exemplo, e você vai experimentar facilmente problemas consideráveis com o PHPUnit: apenas porque o banco de dados é fortemente acoplado com essas bibliotecas.

Você provavelmente conhece esse cenário dos seus trabalhos e projetos diários, onde você quer colocar em prática suas habilidades (novas ou não) com PHPUnit e acaba ficando preso por um dos seguintes problemas:

  1. O método que você quer testar executa uma operação JOIN muito grande e usa os dados para calcular alguns resultados importantes.

  2. Sua lógica de negócios faz uma mistura de declarações SELECT, INSERT, UPDATE e DELETE.

  3. Você precisa definir os dados de teste em (provavelmente muito) mais de duas tabelas para conseguir dados iniciais razoáveis para os métodos que deseja testar.

A extensão DbUnit simplifica consideravelmente a configuração de um banco de dados para fins de teste e permite a você verificar os conteúdos de um banco de dados após fazer uma série de operações. Pode ser instalada da seguinte forma:

pear install phpunit/DbUnit

Fornecedores Suportados para Testes de Banco de Dados

O DbUnit atualmente suporta MySQL, PostgreSQL, Oracle e SQLite. Através das integrações Zend Framework ou Doctrine 2 ele tem acesso a outros sistemas como IBMDB2 ou Microsoft SQL Server.

Dificuldades em Testes de Bancos de Dados

Existe uma boa razão pela qual todos os exemplos de testes unitários não incluírem interações com bancos de dados: esse tipo de testes é complexo tanto em configuração quanto em manutenção. Enquanto testar contra seu banco de dados você precisará ter cuidado com as seguintes variáveis:

  • Esquema e tabelas do banco de dados

  • Inserção das linhas exigidas para o teste nessas tabelas

  • Verificação do estado do banco de dados depois de executar os testes

  • Limpeza do banco de dados para cada novo teste

Por causa de muitas APIs de bancos de dados como PDO, MySQLi ou OCI8 serem incômodos de usar e verbosas para escrever, fazer esses passos manualmente é um completo pesadelo.

O código de teste deve ser o mais curto e preciso possível por várias razões:

  • Você não quer modificar uma considerável quantidade de código de teste por pequenas mudanças em seu código de produção.

  • Você quer ser capaz de ler e entender o código de teste facilmente, mesmo meses depois de tê-lo escrito.

Adicionalmente você tem que perceber que o banco de dados é essencialmente uma variável global de entrada para seu código. Dois testes em sua suíte de testes podem executar contra o mesmo banco de dados, possivelmente reutilizando dados múltiplas vezes. Falhas em um teste podem facilmente afetar o resultado dos testes seguintes, fazendo sua experiência com os testes muito difícil. O passo de limpeza mencionado anteriormente é da maior importância para resolver o problema do banco de dados ser uma entrada global.

O DbUnit ajuda a simplificar todos esses problemas com testes de bancos de dados de forma elegante.

O PHPUnit só não pode ajudá-lo no fato de que testes de banco de dados são muito lentos comparados aos testes que não usam bancos de dados. Dependendo do tamanho das interações com seu banco de dados, seus testes podem levar um tempo considerável para executar. Porém se você mantiver pequena a quantidade de dados usados para cada teste e tentar testar o máximo possível sem usar testes com bancos de dados, você facilmente conseguirá tempos abaixo de um minuto, mesmo para grandes suítes de teste.

A suíte de testes do projeto Doctrine 2 por exemplo, atualmente tem uma suíte com cerca de 1000 testes onde aproximadamente a metade deles tem acesso ao banco de dados e ainda executa em 15 segundos contra um banco de dados MySQL em um computador desktop comum.

Os quatro estágios dos testes com banco de dados

Em seu livro sobre Padrões de Teste xUnit, Gerard Meszaros lista os quatro estágios de um teste unitário:

  1. Configurar o ambiente (fixture)

  2. Exercitar o Sistema Sob Teste

  3. Verificar a saída

  4. Teardown

O que é um ambiente (fixture)?

Descreve o estado inicial em que sua aplicação e seu banco de dados estão ao executar um teste.

Testar um banco de dados exige que você utilize pelo menos o setup e teardown para limpar e escrever em suas tabelas os dados de ambiente exigidos. Porém a extensão do banco de dados tem uma boa razão para reverter esses quatro estágios em um teste de banco de dados para assemelhar o seguinte fluxo de trabalho que é executado para cada teste:

1. Limpar o Banco de Dados

Já que sempre existe um primeiro teste que é executado contra o banco de dados, você não sabe exatamente se já existem dados nas tabelas. O PHPUnit vai executar um TRUNCATE contra todas as tabelas que você especificou para redefinir seus estados para vazio.

2. Configurar o ambiente

O PHPUnit então vai iterar sobre todas as linhas do ambiente especificado e inseri-las em suas respectivas tabelas.

3–5. Executar Teste, Verificar saída e Teardown

Depois de redefinir o banco de dados e carregá-lo com seu estado inicial, o verdadeiro teste é executado pelo PHPUnit. Esta parte do código teste não exige conhecimento sobre a Extensão do Banco de Dados, então você pode prosseguir e testar o que quiser com seu código.

Em seu teste use uma asserção especial chamada assertDataSetsEqual() para fins de verificação, porém isso é totalmente opcional. Esta função será explicada na seção Asserções em Bancos de Dados.

Configuração de Caso de Teste de Banco de Dados do PHPUnit

Ao usar o PHPUnit seus casos de teste vão estender a classe PHPUnit_Framework_TestCase da seguinte forma:

class MeuTest extends PHPUnit_Framework_TestCase
{
    public function testCalculo()
    {
        $this->assertEquals(2, 1 + 1);
    }
}

Se você quer um código de teste que trabalha com a Extensão para Banco de Dados a configuração é um pouco mais complexa e você terá que estender um TestCase abstrato diferente, exigindo que você implemente dois métodos abstratos getConnection() e getDataSet():

class MeuLivroDeVisitasTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $pdo = new PDO('sqlite::memory:');
        return $this->createDefaultDBConnection($pdo, ':memory:');
    }

    /**
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    public function getDataSet()
    {
        return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/guestbook-seed.xml');
    }
}

Implementando getConnection()

Para permitir que a limpeza e o carregamento de funcionalidades do ambiente funcionem, a Extensão do Banco de Dados do PHPUnit exige acesso a uma conexão abstrata do banco de dados através dos fornecedores da biblioteca PDO. É importante notar que sua aplicação não precisa ser baseada em PDO para usar a Extensão para Banco de Dados do PHPUnit, pois a conexão é usada apenas para limpeza e configuração do ambiente.

No exemplo anterior criamos uma conexão Sqlite na memória e a passamos ao método createDefaultDBConnection que embrulha a instância do PDO e o segundo parâmetro (o nome do banco de dados) em uma camada simples de abstração para conexões do banco de dados do tipo PHPUnit_Extensions_Database_DB_IDatabaseConnection.

A seção Usando a Conexão do Banco de Dados explica a API desta interface e como você pode usá-la da melhor forma possível.

Implementando getDataSet()

O método getDataSet() define como deve ser o estado inicial do banco de dados antes de cada teste ser executado. O estado do banco de dados é abstraído através de conceitos Conjunto de Dados e Tabela de Dados, ambos sendo representados pelas interfaces PHPUnit_Extensions_Database_DataSet_IDataSet e PHPUnit_Extensions_Database_DataSet_IDataTable. A próxima seção vai descrever em detalhes como esses conceitos trabalham e quais os benefícios de usá-los nos testes com bancos de dados.

Para a implementação precisaremos apenas saber que o método getDataSet() é chamado uma vez durante o setUp() para recuperar o conjunto de dados do ambiente e inseri-lo no banco de dados. No exemplo estamos usando um método de fábrica createFlatXMLDataSet($nomearquivo) que representa um conjunto de dados através de uma representação XML.

E quanto ao Esquema do Banco de Dados (DDL)?

O PHPUnit assume que o esquema do banco de dados com todas as suas tabelas, gatilhos, sequências e visualizações é criado antes que um teste seja executado. Isso quer dizer que você como desenvolvedor deve se certificar que o banco de dados está corretamente configurado antes de executar a suíte.

Existem vários meios para atingir esta pré-condição para testar bancos de dados.

  1. Se você está usando um banco de dados persistente (não Sqlite Memory) você pode facilmente configurar o banco de dados uma vez com ferramentas como phpMyAdmin para MySQL e reutilizar o banco de dados para cada execução de teste.

  2. Se você estiver usando bibliotecas como Doctrine 2 ou Propel você pode usar suas APIs para criar o esquema de banco de dados que precisa antes de rodar os testes. Você pode utilizar as capacidades de Configuração e Bootstrap do PHPUnit para executar esse código sempre que seus testes forem executados.

Dica: Use seu próprio Caso Abstrato de Teste de Banco de Dados

Do exemplo prévio de implementação você pode facilmente perceber que o método getConnection() é bastante estático e pode ser reutilizado em diferentes casos de teste de banco de dados. Adicionalmente para manter uma boa performance dos seus testes e pouca carga sobre seu banco de dados, você pode refatorar o código um pouco para obter um caso de teste abstrato genérico para sua aplicação, o que ainda permite a você especificar um ambiente de dados diferente para cada caso de teste:

abstract class MinhaApp_Testes_CasosDeTesteDeBancoDeDadosTest extends PHPUnit_Extensions_Database_TestCase
{
    // instancie o pdo apenas uma vez por limpeza de teste/carregamento de ambiente
    static private $pdo = null;

    // instancie PHPUnit_Extensions_Database_DB_IDatabaseConnection apenas uma vez por teste
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO('sqlite::memory:');
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, ':memory:');
        }

        return $this->conn;
    }
}

Entretanto, isso tem a conexão ao banco de dados codificada na conexão do PDO. O PHPUnit tem outra incrível característica que pode fazer este caso de teste ainda mais genérico. Se você usar a Configuração XML você pode tornar a conexão com o banco de dados configurável por execução de teste. Primeiro vamos criar um arquivo phpunit.xml em seu diretório testes da aplicação, de forma semelhante a isto:

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
    <php>
        <var name="BD_DSN" value="mysql:dbname=meulivrodevisitas;host=localhost" />
        <var name="BD_USUARIO" value="usuario" />
        <var name="BD_SENHA" value="senha" />
        <var name="BD_NOMEBD" value="meulivrodevisitas" />
    </php>
</phpunit>

Agora podemos modificar seu caso de teste para parecer com isso:

abstract class Generic_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
    // instancie o pdo apenas uma vez por limpeza de teste/carregamento de ambiente
    static private $pdo = null;

    // instancie PHPUnit_Extensions_Database_DB_IDatabaseConnection apenas uma vez por teste
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO( $GLOBALS['BD_DSN'], $GLOBALS['BD_USUARIO'], $GLOBALS['BD_SENHA'] );
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['BD_NOMEBD']);
        }

        return $this->conn;
    }
}

Agora podemos executar a suíte de testes de banco de dados usando diferentes configurações através da interface de linha-de-comando:

user@desktop> phpunit --configuration developer-a.xml MeusTestes/
user@desktop> phpunit --configuration developer-b.xml MeusTestes/

A possibilidade de executar facilmente os testes de banco de dados contra diferentes alvos é muito importante se você está desenvolvendo na máquina de desenvolvimento. Se vários desenvolvedores executarem os testes de banco de dados contra a mesma conexão de banco de dados você experimentará facilmente falhas de testes devido à condição de execução.

Entendendo Conjunto de Dados e Tabelas de Dados

Um conceito central da Extensão para Banco de Dados do PHPUnit são os Conjuntos de Dados e as Tabelas de Dados. Você deveria tentar entender este conceito simples para dominar os testes de banco de dados com PHPUnit. Conjunto de Dados e Tabela de Dados formam uma camada abstrata em torno das tabelas, linhas e colunas do seu banco de dados. Uma simples API esconde os conteúdos subjacentes do banco de dados em uma estrutura de objetos, que também podem ser implementada por outra fonte que não seja um banco de dados.

Essa abstração é necessária para comparar os conteúdos reais de um banco de dados contra os conteúdos esperados. Expectativas podem ser representadas como arquivos XML, YAML, CSV ou vetores PHP, por exemplo. As interfaces Conjunto de Dados e Tabela de Dados permitem a comparação dessas fontes conceitualmente diferentes, emulando a armazenagem relacional de banco de dados em uma abordagem semanticamente similar.

Um fluxo de trabalho para asserções em banco de dados nos seus testes então consiste de três passos simples:

  • Especificar uma ou mais tabelas em seu banco de dados por nome de tabela (conjunto de dados real);

  • Especificar o Conjunto de Dados esperado no seu formato preferido (YAML, XML, ...);

  • Assertar que ambas as representações de conjunto de dados se equivalem.

Asserções não são o único caso de uso para o Conjunto de Dados e a Tabela de Dados na Extensão para Banco de Dados do PHPUnit. Como mostrado na seção anterior, eles também descrevem os conteúdos iniciais de um banco de dados. Você é forçado a definir um conjunto de dados de ambiente pelo Caso de Teste de Banco de Dados, que então é usado para:

  • Deletar todas as linhas das tabelas especificadas no conjunto de dados.

  • Escrever todas as linhas nas tabelas de dados do banco de dados.

Implementações disponíveis

Existem três tipos diferentes de conjuntos de dados/tabelas de dados:

  • Conjuntos de Dados e Tabelas de Dados baseados em arquivo

  • Conjuntos de Dados e Tabelas de Dados baseados em query

  • Filtro e Composição de Conjunto de Dados e Tabelas de Dados

Os Conjuntos de Dados e Tabelas de Dados baseadas em arquivo são geralmente usadas para o ambiente inicial e para descrever os estados esperados do banco de dados.

Conjunto de Dados de XML Plano

O Conjunto de Dados mais comum é chamada XML Plano. É um formato xml simples onde uma tag dentro do nó-raiz <dataset> representa exatamente uma linha no banco de dados. Os nomes das tags equivalem à tabela onde inserir a linha e um atributo representa a coluna. Um exemplo para uma simples aplicação de livro de visitas poderia se parecer com isto:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas id="1" conteudo="Olá amigo!" usuario="joao" criado="2010-04-24 17:15:23" />
    <livrodevisitas id="2" conteudo="Eu gostei!" usuario="nanda" criado="2010-04-26 12:14:20" />
</dataset>

Isso é, obviamente, fácil de escrever. Aqui <livrodevisitas> é o nome da tabela onde duas linhas são inseridas dentro de cada com quatro colunas id, conteudo, usuario e criado com seus respectivos valores.

Porém essa simplicidade tem um preço.

O exemplo anterior não deixa tão óbvio como você pode fazer para especificar uma tabela vazia. Você pode inserir uma tag sem atributos com o nome da tabela vazia. Um arquivo xml plano para uma tabela vazia do livro de visitas ficaria parecido com isso:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas />
</dataset>

O manejamento de valores NULL com o xml plano é tedioso. Um valor NULL é diferente de uma string com valor vazio em quase todos os bancos de dados (Oracle é uma exceção), algo difícil de descrever no formato xml plano. Você pode representar um valor NULL omitindo o atributo da especificação da linha. Se seu livro de visitas vai permitir entradas anônimas representadas por um valor NULL na coluna usuario, um estado hipotético para a tabela do livrodevisitas seria parecido com:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas id="1" conteudo="Olá amigo!" usuario="joao" criado="2010-04-24 17:15:23" />
    <livrodevisitas id="2" conteudo="Eu gostei!" criado="2010-04-26 12:14:20" />
</dataset>

Nesse caso a segunda entrada é postada anonimamente. Porém isso acarreta um problema sério no reconhecimento de colunas. Durante as asserções de igualdade do conjunto de dados, cada conjunto de dados tem que especificar quais colunas uma tabela possui. Se um atributo for NULL para todas as linhas de uma tabela de dados, como a Extensão para Banco de Dados vai saber que a coluna pode ser parte da tabela?

O conjunto de dados em xml plano faz uma presunção crucial agora, definindo que os atributos na primeira linha definida de uma tabela define as colunas dessa tabela. No exemplo anterior isso significaria que id, conteudo, usuario e criado são colunas da tabela livrodevisitas. Para a segunda linha, onde usuario não está definido, um NULL seria inserido no banco de dados.

Quando a primeira entrada do livrodevisitas for apagada do conjunto de dados, apenas id, conteudo e criado seriam colunas da tabela livrodevisitas, já que usuario não é especificado.

Para usar o Conjunto de Dados em XML Plano efetivamente, quando valores NULL forem relevantes, a primeira linha de cada tabela não deve conter qualquer valor NULL e apenas as linhas seguintes poderão omitir atributos. Isso pode parecer estranho, já que a ordem das linhas é um fator relevante para as asserções com bancos de dados.

Em troca, se você especificar apenas um subconjunto das colunas da tabela no Conjunto de Dados do XML Plano, todos os valores omitidos serão definidos para seus valores padrão. Isso vai induzir a erros se uma das colunas omitidas estiver definida como NOT NULL DEFAULT NULL.

Para concluir eu posso dizer que as conjuntos de dados XML Plano só devem ser usadas se você não precisar de valores NULL.

Você pode criar uma instância de conjunto de dados xml plano de dentro de seu Caso de Teste de Banco de Dados chamando o método createFlatXmlDataSet($nomearquivo):

class MeuCasoDeTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createFlatXmlDataSet('meuAmbienteXmlPlano.xml');
    }
}

Conjunto de Dados XML

Existe uma outro Conjunto de Dados em XML mais estruturado, que é um pouco mais verboso para escrever mas evita os problemas do NULL nos conjuntos de dados em XML Plano. Dentro do nó-raiz <dataset> você pode especificar as tags <table>, <column>, <row>, <value> e <null />. Um Conjunto de Dados equivalente ao definida anteriormente no Livrodevisitas em XML Plano seria como:

<?xml version="1.0" ?>
<dataset>
    <table name="livrodevisitas">
        <column>id</column>
        <column>conteudo</column>
        <column>usuario</column>
        <column>criado</column>
        <row>
            <value>1</value>
            <value>Olá amigo!</value>
            <value>joao</value>
            <value>2010-04-24 17:15:23</value>
        </row>
        <row>
            <value>2</value>
            <value>Eu gostei!</value>
            <null />
            <value>2010-04-26 12:14:20</value>
        </row>
    </table>
</dataset>

Qualquer <table> definida tem um nome e requer uma definição de todas as colunas com seus nomes. Pode conter zero ou qualquer número positivo de elementos aninhados <row>. Não definir nenhum elemento <row> significa que a tabela está vazia. As tags <value> e <null /> têm que ser especificadas na ordem especificada nos elementos fornecidos previamente em <column>. A tag <null /> obviamente significa que o valor é NULL.

Você pode criar uma instância de conjunto de dados xml de dentro de seu Caso de Teste de Banco de Dados chamando o método createXmlDataSet($nomearquivo):

class MeuCasoDeTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createXMLDataSet('meuAmbienteXml.xml');
    }
}

Conjunto de Dados XML MySQL

Este novo formato de arquivo XML é específico para o servidor de banco de dados MySQL. O suporte para ele foi adicionado no PHPUnit 3.5. Arquivos nesse formato podem ser gerados usando o utilitáriomysqldump. Diferente dos conjuntos de dados CSV, que o mysqldump também suporta, um único arquivo neste formato XML pode conter dados para múltiplas tabelas. Você pode criar um arquivo nesse formato invocando o mysqldump desta forma:

mysqldump --xml -t -u [nomeusuario] --password=[senha] [bancodedados] > /caminho/para/arquivo.xml
        

Esse arquivo pode ser usado em seu Caso de Teste de Banco de Dados chamando o método createMySQLXMLDataSet($filename):

class MeuCasoDeTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createMySQLXMLDataSet('/caminho/para/arquivo.xml');
    }
}

Conjunto de Dados YAML

Novidade do PHPUnit 3.4, é a capacidade de especificar um Conjunto de Dados no popular formato YAML. Para funcionar, você deve instalar o PHPUnit 3.4 através do PEAR com a dependência opcional do Symfony YAML. Então você poderá escrever um Conjunto de Dados YAML para o exemplo do livrodevisitas:

livrodevisitas:
  -
    id: 1
    conteudo: "Olá amigo!"
    usuario: "joao"
    criado: 2010-04-24 17:15:23
  -
    id: 2
    conteudo: "Eu gostei!"
    usuario:
    criado: 2010-04-26 12:14:20

Isso é simples, conveniente E resolve o problema do NULL que o Conjunto de Dados similar do XML Plano tem. Um NULL em um YAML é apenas o nome da coluna sem nenhum valor especificado. Uma string vazia é especificada como coluna1: "".

O Conjunto de Dados YAML atualmente não possui método de fábrica no Caso de Teste de Banco de Dados, então você tem que instanciar manualmente:

class LivroDeVisitasYamlTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        return new PHPUnit_Extensions_Database_DataSet_YamlDataSet(
            dirname(__FILE__)."/_files/livrodevisitas.yml"
        );
    }
}

Conjunto de Dados CSV

Outro Conjunto de Dados baseado em arquivo, com arquivos CSV. Cada tabela do conjunto de dados é representada como um único arquivo CSV. Para nosso exemplo do livrodevisitas, vamos definir um arquivo tabela-livrodevisitas.csv:

id,conteudo,usuario,criado
1,"Olá amigo!","joao","2010-04-24 17:15:23"
2,"Eu gostei!","nanda","2010-04-26 12:14:20"

Apesar disso ser muito conveniente para edição no Excel ou OpenOffice, você não pode especificar valores NULL em um Conjunto de Dados CSV. Uma coluna vazia levaria a um valor vazio padrão ser inserido na coluna.

Você pode criar um Conjunto de Dados CSV chamando:

class LivroDeVisitasCsvTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        $conjuntoDados = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
        $conjuntoDados->addTable('livrodevisitas', dirname(__FILE__)."/_files/livrodevisitas.csv");
        return $conjuntoDados;
    }
}

Vetor de Conjunto de Dados

Não existe (ainda) Conjunto de Dados baseado em vetor na Extensão para Banco de Dados do PHPUnit, mas podemos implementar o nosso próprio facilmente. Nosso exemplo do livrodevisitas deveria ficar parecido com:

class VetorLivroDeVisitasTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        return new MinhaApp_DbUnit_VetorConjuntoDeDados(array(
            'livrodevisitas' => array(
                array('id' => 1, 'conteudo' => 'Olá amigo!', 'usuario' => 'joao', 'criado' => '2010-04-24 17:15:23'),
                array('id' => 2, 'conteudo' => 'Eu gostei!', 'usuario' => null, 'criado' => '2010-04-26 12:14:20'),
            ),
        ));
    }
}

Um Conjunto de Dados do PHP em algumas vantagens óbvias sobre todas as outras conjuntos de dados baseadas em arquivos:

  • Vetores PHP podem, obviamente, trabalhar com valores NULL.

  • Você não precisa de arquivos adicionais para asserções e pode especificá-las diretamente no Caso de Teste.

Para este Conjunto de Dados, como nos Conjunto de Dados em XML Plano, CSV e YAML, as chaves da primeira linha especificada definem os nomes das colunas das tabelas, que no caso anterior seriam id, conteudo, usuario e criado.

A implementação para este vetor Conjunto de Dados é simples e direta:

class MinhaApp_DbUnit_VetorConjuntoDeDadosTest extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet
{
    /**
     * @var array
     */
    protected $tabelas = array();

    /**
     * @param array $dados
     */
    public function __construct(array $dados)
    {
        foreach ($dados AS $nomeTabela => $linhas) {
            $colunas = array();
            if (isset($linhas[0])) {
                $colunas = array_keys($linhas[0]);
            }

            $metaDados = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($nomeTabela, $colunas);
            $tabela = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaDados);

            foreach ($linhas AS $linha) {
                $tabela->addRow($linha);
            }
            $this->tabelas[$nomeTabela] = $tabela;
        }
    }

    protected function createIterator($reverso = FALSE)
    {
        return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tabelas, $reverso);
    }

    public function getTable($nomeTabela)
    {
        if (!isset($this->tabelas[$nomeTabela])) {
            throw new InvalidArgumentException("$nomeTabela não é uma tabela do banco de dados atual.");
        }

        return $this->tabelas[$nomeTabela];
    }
}

Conjunto de Dados Query (SQL)

Para asserções de banco de dados você não precisa somente de conjuntos de dados baseados em arquivos, mas também de conjuntos de dados baseados em Query/SQL que contenha os conteúdos reais do banco de dados. É aí que entra o Query DataSet:

$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('livrodevisitas');

Adicionar uma tabela apenas por nome é um modo implícito de definir a tabela de dados com a seguinte query:

$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('livrodevisitas', 'SELECT * FROM livrodevisitas');

Você pode fazer uso disso especificando querys arbitrárias para suas tabelas, por exemplo restringindo linhas, colunas, ou adicionando cláusulas ORDER BY:

$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('livrodevisitas', 'SELECT id, conteudo FROM livrodevisitas ORDER BY criado DESC');

A seção nas Asserções de Banco de Dados mostrará mais alguns detalhes sobre como fazer uso do Conjunto de Dados Query.

Conjunto de Dados de Banco de Dados (BD)

Acessando a Conexão de Teste você pode criar automaticamente um Conjunto de Dados que consiste de todas as tabelas com seus conteúdos no banco de dados especificado como segundo parâmetro ao método Conexões de Fábrica.

Você pode tanto criar um Conjunto de Dados para todo o banco de dados como mostrado em testLivrodevisitas(), ou restringi-lo a um conjunto de nomes específicos de tabelas com uma lista branca, como mostrado no método testLivrodevisitasFiltrado().

class MeuTesteLivrodevisitasMySqlTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $bancodedados = 'meu_bancodedados';
        $pdo = new PDO('mysql:...', $usuario, $senha);
        return $this->createDefaultDBConnection($pdo, $bancodedados);
    }

    public function testLivrodevisitas()
    {
        $conjuntoDados = $this->getConnection()->createDataSet();
        // ...
    }

    public function testLivrodevisitasFiltrado()
    {
        $nomesTabelas = array('livrodevisitas');
        $conjuntoDados = $this->getConnection()->createDataSet($nomesTabelas);
        // ...
    }
}

Conjunto de Dados de Reposição

Eu tenho falado sobre problemas com NULL nos Conjunto de Dados XML Plano e CSV, mas existe uma alternativa um pouco complicada para fazer ambos funcionarem com NULLs.

O Conjunto de Dados de Reposição é um decorador para um Conjunto de Dados existente e permite que você substitua valores em qualquer coluna do conjunto de dados por outro valor de reposição. Para fazer nosso exemplo do livro de visitas funcionar com valores NULL devemos especificar o arquivo como:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas id="1" conteudo="Olá amigo!" usuario="joe" criado="2010-04-24 17:15:23" />
    <livrodevisitas id="2" conteudo="Eu gostei!" usuario="##NULL##" criado="2010-04-26 12:14:20" />
</dataset>

Então envolvemos o Conjunto de Dados em XML Plano dentro de um Conjunto de Dados de Reposição:

class ReposicaoTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        $ds = $this->createFlatXmlDataSet('meuAmbienteXmlPlano.xml');
        $rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
        $rds->addFullReplacement('##NULL##', null);
        return $rds;
    }
}

Filtro de Conjunto de Dados

Se você tiver um arquivo grande de ambiente você pode usar o Filtro de Conjunto de Dados para as listas branca e negra das tabelas e colunas que deveriam estar contidas em um sub-conjunto de dados. Isso ajuda especialmente em combinação com o BD DataSet para filtrar as colunas dos conjuntos de dados.

class FiltroConjuntoDeDadosTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testIncluirLivrodevisitasFiltrado()
    {
        $nomesTabelas = array('livrodevisitas');
        $conjuntoDados = $this->getConnection()->createDataSet();

        $filtroConjuntoDeDados = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($conjuntoDados);
        $filtroConjuntoDeDados->addIncludeTables(array('livrodevisitas'));
        $filtroConjuntoDeDados->setIncludeColumnsForTable('livrodevisitas', array('id', 'conteudo'));
        // ..
    }

    public function testExcluirLivrodevisitasFiltrado()
    {
        $nomesTabelas = array('livrodevisitas');
        $conjuntoDeDados = $this->getConnection()->createDataSet();

        $filtroConjuntoDeDados = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($conjuntoDeDados);
        $filtroConjuntoDeDados->addExcludeTables(array('foo', 'bar', 'baz')); // mantém apenas a tabela livrodevisitas!
        $filtroConjuntoDeDados->setExcludeColumnsForTable('livrodevisitas', array('usuario', 'criado'));
        // ..
    }
}

NOTA Você não pode usar ambos os filtros de coluna excluir e incluir na mesma tabela, apenas em tabelas diferentes. E mais: só é possível para a lista branca ou negra, mas não para ambas.

Conjunto de Dados Composto

O Conjunto de Dados composto é muito útil para agregar vários conjuntos de dados já existentes em um único Conjunto de Dados. Quando vários conjuntos de dados contém as mesmas tabelas, as linhas são anexadas na ordem especificada. Por exemplo se tivermos dois conjuntos de dados ambiente1.xml:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas id="1" conteudo="Olá amigo!" usuario="joao" criado="2010-04-24 17:15:23" />
</dataset>

e ambiente2.xml:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas id="2" conteudo="Eu gostei!" usuario="##NULL##" criado="2010-04-26 12:14:20" />
</dataset>

Usando o Conjunto de Dados Composto podemos agregar os dois arquivos de ambiente:

class CompostoTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        $ds1 = $this->createFlatXmlDataSet('ambiente1.xml');
        $ds2 = $this->createFlatXmlDataSet('ambiente2.xml');

        $dsComposta = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();
        $dsComposta->addDataSet($ds1);
        $dsComposta->addDataSet($ds2);

        return $dsComposta;
    }
}

Cuidado com Chaves Estrangeiras

Durante a Configuração do Ambiente a Extensão para Banco de Dados do PHPUnit insere as linhas no banco de dados na ordem que são especificadas em seu ambiente. Se seu esquema de banco de dados usa chaves estrangeiras isso significa que você tem que especificar as tabelas em uma ordem que não faça as restrições das chaves estrangeiras falharem.

Implementando seus próprios Conjuntos de Dados/ Tabelas de Dados

Para entender os interiores dos Conjuntos de Dados e Tabelas de Dados, vamos dar uma olhada na interface de um Conjunto de Dados. Você pode pular esta parte se você não planeja implementar sua próprio Conjunto de Dados ou Tabela de Dados.

interface PHPUnit_Extensions_Database_DataSet_IDataSet extends IteratorAggregate
{
    public function getTableNames();
    public function getTableMetaData($nomeTabela);
    public function getTable($nomeTabela);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_IDataSet $outra);

    public function getReverseIterator();
}

A interface pública é usada internamente pela asserção assertDataSetsEqual() no Caso de Teste de Banco de Dados para verificar a qualidade do conjunto de dados. Da interface IteratorAggregate o IDataSet herda o método getIterator() para iterar sobre todas as tabelas do conjunto de dados. O método adicional do iterador reverso é requerido para truncar corretamente as tabelas em ordem reversa à especificada.

Para entender a necessidade de um iterador reverso pense em duas tabelas (TabelaA e TabelaB) onde TabelaB possui uma chave estrangeira em uma coluna da TabelaA. Se para a configuração do ambiente uma linha é inserida na TabelaA e então um registro dependente na TabelaB, então é óbvio que para deletar todos os conteúdos das tabelas a execução em ordem reversa vai causar problemas com as restrições de chaves estrangeiras.

Dependendo da implementação, diferentes abordagens são usadas para adicionar instâncias de tabela a um Conjunto de Dados. Por exemplo, tabelas são adicionadas internamente durante a construção a partir de um arquivo fonte em todas as conjuntos de dados baseadas em arquivo como YamlDataSet, XmlDataSet ou FlatXmlDataSet.

Uma tabela também é representada pela seguinte interface:

interface PHPUnit_Extensions_Database_DataSet_ITable
{
    public function getTableMetaData();
    public function getRowCount();
    public function getValue($linha, $coluna);
    public function getRow($linha);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_ITable $outro);
}

Com exceção do método getTabelaMetaDados() isso é bastante auto-explicativo. Os métodos usados são todos requeridos para as diferentes asserções da Extensão para Banco de Dados que são explicados no próximo capítulo. O método getTabelaMetaDados() deve retornar uma implementação da interface PHPUnit_Extensions_Database_DataSet_ITableMetaData que descreve a estrutura da tabela. Possui informações sobre:

  • O nome da tabela

  • Um vetor de nomes de coluna da tabela, ordenado por suas aparições nos resultados

  • Um vetor de colunas de chaves-primárias.

Essa interface também tem uma asserção que verifica se duas instâncias da Tabela Metadados se equivalem, o que é usado pela asserção de igualdade do conjunto de dados.

A API de Conexão

Existem três métodos interessantes na interface de conexão que devem ser retornados do método getConnection() no Caso de Teste de Banco de Dados:

interface PHPUnit_Extensions_Database_DB_IDatabaseConnection
{
    public function createDataSet(Array $nomesTabelas = NULL);
    public function createQueryTable($nomeResultado, $sql);
    public function getRowCount($nomeTabela, $clausulaWhere = NULL);

    // ...
}
  1. O método createDataSet() cria um Conjunto de Dados de Banco de Dados (BD) como descrito na seção de implementações de Conjunto de Dados.

    class ConexaoTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCriarConjuntoDeDados()
        {
            $nomesTabelas = array('livrodevisitas');
            $conjuntoDados = $this->getConnection()->createDataSet();
        }
    }
    
  2. O método createQueryTable() 2. pode ser usado para criar instâncias de uma TabelaQuery, dê a eles um nome de resultado e uma query SQL. Este é um método conveniente quando se fala sobre asserções de tabela/resultado como será mostrado na próxima seção de API de Asserções de Banco de Dados.

    class ConexaoTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCriarTabelaQuery()
        {
            $nomesTabela = array('livrodevisitas');
            $tabelaQuery = $this->getConnection()->createQueryTable('livrodevisitas', 'SELECT * FROM livrodevisitas');
        }
    }
    
  3. O método getRowCount() é uma forma conveniente de acessar o número de linhas em uma tabela, opcionalmente filtradas por uma cláusula where adicional. Isso pode ser usado com uma simples asserção de igualdade:

    class ConexaoTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testGetContagemLinhas()
        {
            $this->assertEquals(2, $this->getConnection()->getRowCount('livrodevisitas'));
        }
    }
    

API de Asserções de Banco de Dados

Para uma ferramenta de testes, a Extensão para Banco de Dados certamente fornece algumas asserções que você pode usar para verificar o estado atual do banco de dados, tabelas e a contagem de linhas de tabelas. Esta seção descreve essa funcionalidade em detalhes:

Assertando a contagem de linhas de uma Tabela

Às vezes ajuda verificar se uma tabela contém uma quantidade específica de linhas. Você pode conseguir isso facilmente sem colar códigos adicionais usando a API de Conexão. Suponha que queiramos verificar se após a inserção de uma linha em nosso livro de visitas não apenas temos as duas entradas iniciais que nos acompanharam em todos os exemplos anteriores, mas uma terceira:

class LivroDeVisitasTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testAdicionaEntrada()
    {
        $this->assertEquals(2, $this->getConnection()->getContagemLinhas('livrodevisitas'), "Pre-Condition");

        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Olá mundo!");

        $this->assertEquals(3, $this->getConnection()->getContagemLinhas('livrodevisitas'), "Inserção falhou");
    }
}

Assertando o Estado de uma Tabela

A asserção anterior ajuda, mas certamente queremos verificar os conteúdos reais da tabela para verificar se todos os valores foram escritos nas colunas corretas. Isso pode ser conseguido com uma asserção de tabela.

Para isso vamos definir uma instância de Tabela Query que deriva seu conteúdo de um nome de tabela e de uma query SQL e compara isso a um Conjunto de Dados baseado em vetor/Arquivo:

class LivroDeVisitasTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testAdicionaEntrada()
    {
        $livrodevisitas = new LivrodeVisitas();
        $livrodevisitas->addEntry("suzy", "Olá mundo!");

        $tabelaQuery = $this->getConnection()->createQueryTable(
            'livrodevisitas', 'SELECT * FROM livrodevisitas'
        );
        $tabelaEsperada = $this->createFlatXmlDataSet("livroEsperado.xml")
                              ->getTable("livrodevisitas");
        $this->assertTablesEqual($tabelaEsperada, $tabelaQuery);
    }
}

Agora temos que escrever o arquivo XML Plano livroEsperado.xml para esta asserção:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas id="1" conteudo="Olá amigo!" usuario="joao" criado="2010-04-24 17:15:23" />
    <livrodevisitas id="2" conteudo="Eu gostei!" usuario="nanda" criado="2010-04-26 12:14:20" />
    <livrodevisitas id="3" conteudo="Olá mundo!" usuario="suzy" criado="2010-05-01 21:47:08" />
</dataset>

Apesar disso, esta asserção só vai passar em exatamente um segundo do universo, em 2010–05–01 21:47:08. Datas possuem um problema especial nos testes de bancos de dados e podemos circundar a falha omitindo a coluna criado da asserção.

O arquivo ajustado livroEsperado.xml em XML Plano provavelmente vai ficar parecido com o seguinte para fazer a asserção passar:

<?xml version="1.0" ?>
<dataset>
    <livrodevisitas id="1" conteudo="Olá amigo!" usuario="joao" />
    <livrodevisitas id="2" conteudo="Eu gostei!" usuario="nanda" />
    <livrodevisitas id="3" conteudo="Olá mundo!" usuario="suzy" />
</dataset>

Nós temos que consertar a chamada da Tabela Query:

$tabelaQuery = $this->getConnection()->createQueryTable(
    'livrodevisitas', 'SELECT id, conteudo, usuario FROM livrodevisitas'
);

Assertando o Resultado de uma Query

Você também pode assertar o resultado de querys complexas com a abordagem da Tabela Query, apenas especificando um nome de resultado com uma query e comparando isso a um conjunto de dados.

class QueryComplexaTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testQueryComplexa()
    {
        $tabelaQuery = $this->getConnection()->createQueryTable(
            'minhaQueryComplexa', 'SELECT queryComplexa...'
        );
        $tabelaEsperada = $this->createFlatXmlDataSet("assercaoQueryComplexa.xml")
                              ->getTable("minhaQueryComplexa");
        $this->assertTablesEqual($tabelaEsperada, $tabelaQuery);
    }
}

Assertando o Estado de Múltiplas Tabelas

Certamente você pode assertar o estado de múltiplas tabelas de uma vez e comparar um conjunto de dados de query contra um conjunto de dados baseado em arquivo. Existem duas formas diferentes de asserções de Conjuntos de Dados.

  1. Você pode usar o Conjunto de Dados de Banco de Dados (BD) e compará-lo com um Conjunto de Dados Baseado em Arquivo.

    class AssercaoConjuntoDeDadosTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCriarAssercaoDeConjuntoDeDados()
        {
            $conjuntoDeDados = $this->getConnection()->createDataSet(array('livrodevisitas'));
            $conjuntoDeDadosEsperado = $this->createFlatXmlDataSet('livrodevisitas.xml');
            $this->assertDataSetsEqual($conjuntoDeDadosEsperado, $conjuntoDeDados);
        }
    }
    
  2. Você pode construir o Conjunto de Dados por si mesmo:

    class AssercoesConjuntoDeDadosTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testAssercaoManualDeConjuntoDeDados()
        {
            $conjuntoDeDados = new PHPUnit_Extensions_Database_DataSet_QueryDataSet();
            $conjuntoDeDados->addTable('livrodevisitas', 'SELECT id, conteudo, usuario FROM livrodevisitas'); // tabelas adicionais
            $conjuntoDeDadosEsperado = $this->createFlatXmlDataSet('livrodevisitas.xml');
    
            $this->assertDataSetsEqual($conjuntoDeDadosEsperado, $conjuntoDeDados);
        }
    }
    

Perguntas Mais Frequentes

O PHPUnit vai (re)criar o esquema do banco de dados para cada teste?

Não, o PHPUnit exige que todos os objetos do banco de dados estejam disponíveis quando a suíte começar os testes. O Banco de Dados, tabelas, sequências, gatilhos e visualizações devem ser criadas antes que você execute a suíte de testes.

Doctrine 2 ou eZ Components possuem ferramentas poderosas que permitem a você criar o esquema de banco de dados através de estruturas de dados pré-definidas, porém devem ser ligados à extensão do PHPUnit para permitir a recriação automática de banco de dados antes que a suíte de testes completa seja executada.

Já que cada teste limpa completamente o banco de dados, você nem sequer é forçado a recriar o banco de dados para cada execução de teste. Um banco de dados permanentemente disponível funciona perfeitamente.

Sou forçado a usar PDO em minha aplicação para que a Extensão para Banco de Dados funcione?

Não, PDO só é exigido para limpeza e configuração do ambiente e para asserções. Você pode usar qualquer abstração de banco de dados que quiser dentro de seu próprio código.

O que posso fazer quando recebo um Erro Too much Connections?

Se você não armazena em cache a instância de PDO que é criada a partir do método do Caso de Teste getConnection() o número de conexões ao banco de dados é aumentado em um ou mais com cada teste do banco de dados. Com a configuração padrão o MySQL só permite 100 conexões concorrentes e outros fornecedores também têm um limite máximo de conexões.

A Sub-seção Use seu próprio Caso Abstrato de Teste de Banco de Dados mostra como você pode prevenir o acontecimento desse erro usando uma instância única armazenada em cache do PDO em todos os seus testes.

Como lidar com NULL usando Conjuntos de Dados XML Plano / CSV?

Não faça isso. Em vez disso, você deveria usar Conjuntos de Dados ou XML ou YAML.