O desenvolvimento orientado a testes e os testes unitários são as formas mais recentes de garantir que o código continue funcionando conforme o esperado, apesar de modificações e ajustes importantes. Neste artigo, você aprenderá como testar a unidade do seu código PHP nas camadas do módulo, do banco de dados e da interface do usuário (IU).
São 3 horas da manhã. Como sabemos que nosso código ainda está funcionando?
Os aplicativos da Web rodam 24 horas por dia, 7 dias por semana, então a questão de saber se meu programa ainda está em execução me incomodará à noite. O teste de unidade me ajudou a construir confiança suficiente em meu código para que eu possa dormir bem.
O teste de unidade é uma estrutura para escrever casos de teste para seu código e executar esses testes automaticamente. O desenvolvimento orientado a testes é uma abordagem de teste unitário baseada na ideia de que você deve primeiro escrever testes e verificar se esses testes podem encontrar erros, e só então começar a escrever o código que precisa passar nesses testes. Quando todos os testes forem aprovados, o recurso que desenvolvemos estará completo. O valor desses testes de unidade é que podemos executá-los a qualquer momento — antes de fazer check-in do código, após uma grande alteração ou após a implantação em um sistema em execução.
Teste de Unidade PHP
Para PHP, a estrutura de teste de unidade é PHPUnit2. Este sistema pode ser instalado como um módulo PEAR usando a linha de comando PEAR: % pear install PHPUnit2.
Após instalar a estrutura, você pode escrever testes de unidade criando classes de teste derivadas de PHPUnit2_Framework_TestCase.
Teste de unidade de módulo
Descobri que o melhor lugar para iniciar o teste de unidade é no módulo de lógica de negócios do aplicativo. Estou usando um exemplo simples: esta é uma função que soma dois números. Para iniciar o teste, primeiro escrevemos o caso de teste conforme mostrado abaixo.
Listagem 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd estende PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == 2 }}?>
Esta classe TestAdd possui dois métodos, ambos usam o prefixo test. Cada método define um teste, que pode ser tão simples quanto a Listagem 1 ou muito complexo. Neste caso, simplesmente afirmamos que 1 mais 2 é igual a 3 no primeiro teste e 1 mais 1 é igual a 2 no segundo teste.
O sistema PHPUnit2 define o método assertTrue(), que é usado para testar se o valor da condição contido no parâmetro é verdadeiro. Em seguida, escrevemos o módulo Add.php, que inicialmente produziu resultados incorretos.
Listagem 2. Add.php
<?phpfunction add( $a, $b ) { return 0 }?>
Agora, ao executar os testes de unidade, ambos os testes falham.
Listagem 3. Falhas de teste
% phpunit TestAdd.phpPHPUnit 2.2.1 por Sebastian Bergmann.FFTime: 0.0031270980834961Houve 2 falhas:1) test1(TestAdd)2) test2(TestAdd)FALHAS!!!Testes executados: 2, Falhas: 2, Erros: 0, Testes Incompletos: 0.
Agora sei que ambos os testes funcionam bem. Portanto, a função add() pode ser modificada para realmente fazer a coisa real.
Ambos os testes agora passam.
<?phpfunction add( $a, $b ) { return $a+$b }?>
Listagem 4. Teste aprovado
% phpunit TestAdd.phpPHPUnit 2.2.1 por Sebastian Bergmann...Tempo: 0,0023679733276367OK (2 testes)%
embora Este exemplo de desenvolvimento orientado a testes é muito simples, mas podemos entender suas ideias. Primeiro criamos o caso de teste e tínhamos código suficiente para executar o teste, mas o resultado estava errado. Em seguida, verificamos se o teste realmente falhou e implementamos o código real para fazer o teste passar.
Acho que, à medida que implemento o código, continuo adicionando código até ter um teste completo que cubra todos os caminhos do código. No final deste artigo, você encontrará alguns conselhos sobre quais testes escrever e como escrevê-los.
Teste de banco de dados
Após o teste do módulo, o teste de acesso ao banco de dados pode ser executado. Os testes de acesso ao banco de dados levantam duas questões interessantes. Primeiro, devemos restaurar o banco de dados para algum ponto conhecido antes de cada teste. Em segundo lugar, esteja ciente de que esta recuperação pode causar danos ao banco de dados existente, por isso devemos testar em um banco de dados que não seja de produção ou ter cuidado para não afetar o conteúdo do banco de dados existente ao escrever casos de teste.
O teste de unidade do banco de dados começa no banco de dados. Para ilustrar esse problema, precisamos usar o seguinte padrão simples.
Listagem 5. Schema.sql
DROP TABLE IF EXISTS autores;CREATE TABLE autores (id MEDIUMINT NOT NULL AUTO_INCREMENT,
nome TEXT NOT NULL, PRIMARY KEY (id));
A seguir, você pode escrever casos de teste.
Listagem 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAuthors estende PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } função test_insert() { $this->assertTrue( Autores::delete_all() ); $this->assertTrue( Autores::insert( 'Jack' ) } função test_insert_and_get() { $this->assertTrue( Autores ::delete_all() ); $this->assertTrue( Autores::insert( 'Jack' ) ); $this->assertTrue( Autores::insert( 'Joe' ) ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 );>
Este conjunto de testes cobre a exclusão de autores da tabela e a inserção de autores na tabela. Bem como funções como inserir o autor enquanto verifica se o autor existe. Este é um teste cumulativo, que considero muito útil para encontrar bugs. Ao observar quais testes funcionam e quais não funcionam, você pode descobrir rapidamente o que está errado e entender melhor as diferenças.
A versão do código de acesso ao banco de dados PHP dblib.php que originalmente produziu a falha é mostrada abaixo.
Listagem 7. dblib.php
<?phprequire_once('DB.php'); class Autores{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; :Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage() } return $db } public static function delete_all() { return false; } public static function insert( $name ) { return false } public static function get_all() { return null }}?>
A execução de testes de unidade no código da Listagem 8 mostrará que todos os três testes falharam:
Listagem 8. dblib. php
% phpunit TestAuthors.phpPHPUnit 2.2.1 por Sebastian Bergmann.FFFTempo: 0,007500171661377Houve 3 falhas:1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)FALHAS!!!Testes executados: 3, Falhas: 3, Erros: 0, Testes Incompletos: 0.%
Agora podemos começar a adicionar o código para acessar corretamente o banco de dados - método por método - até que todos os 3 testes sejam aprovados. A versão final do código dblib.php é mostrada abaixo.
Listagem 9. Complete dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; ::Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage() } return $db } public static function delete_all() { $ db; = Autores::get_db(); $sth = $db->prepare( 'DELETE FROM autores' ); $db->execute( $sth } public static function insert( $name ) { $db = Autores::get_db(); $sth = $db->prepare( 'INSERT INTO autores VALUES (null,?)' ); $db->execute( $sth, array( $name ) } public; função estática get_all() { $db = Autores::get_db(); $res = $db->query( "SELECT * FROM autores" ); ) ) { $rows []= $row; } return $rows }}?>
Teste de HTML
A próxima etapa no teste de todo o aplicativo PHP é testar a interface front-end de Hypertext Markup Language (HTML). Para realizar este teste, precisamos de uma página Web como a mostrada abaixo.
Listagem 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage estende PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); ->get( $url ); $resp = $client->currentResponse(); return $resp['body'] } function test_get() { $page = TestPage::get_page( 'http://localhost/unit' /add.php' ); $this->assertTrue( strlen( $page ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $page ) == 1 ); ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ); $this->assertTrue( strlen( $page ) > 0 ); assertTrue( preg_match( '/<html>/', $page ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $page, $out ); $this->assertTrue( $out[1]=='30' }}?>
Este teste usa o módulo HTTP Client fornecido pelo PEAR. Acho que é um pouco mais simples do que a Biblioteca de URL do Cliente PHP (CURL) integrada, mas a última também pode ser usada.
Existe um teste que verifica a página retornada e determina se ela contém HTML. O segundo teste solicita a soma de 10 e 20 colocando o valor na URL solicitada e depois verifica o resultado na página retornada.
O código desta página é mostrado abaixo.
Listagem 11. TestPage.php
<html><body><form><input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =<span id="result"><?php echo($_REQUEST ['a']+$_REQUEST['b']); ?></span><br/><input type="submit" value="Adicionar" /></form></body></html >
Esta página é bastante simples. Os dois campos de entrada exibem os valores atuais fornecidos na solicitação. O intervalo de resultados mostra a soma desses dois valores. A marcação marca todas as diferenças: é invisível para o usuário, mas visível para os testes unitários. Portanto, os testes unitários não precisam de lógica complexa para encontrar esse valor. Em vez disso, recupera o valor de uma tag específica. Desta forma, quando a interface mudar, enquanto existir o span, o teste será aprovado.
Como antes, escreva primeiro o caso de teste e depois crie uma versão com falha da página. Testamos se há falhas e depois modificamos o conteúdo da página para que funcione. Os resultados são os seguintes:
Listagem 12. Falha no teste e, em seguida, modifique a página
% phpunit TestPage.phpPHPUnit 2.2.1 por Sebastian Bergmann...Tempo: 0,25711488723755OK (2 testes)%
Ambos os testes podem passar, o que significa que o teste código Funciona bem.
Ao executar testes neste código, todos os testes são executados sem problemas, portanto sabemos que nosso código funciona corretamente.
Mas testar o front-end HTML tem uma falha: JavaScript. O código do cliente HTTP (Hypertext Transfer Protocol) recupera a página, mas não executa o JavaScript. Portanto, se tivermos muito código em JavaScript, teremos que criar testes de unidade no nível do agente do usuário. A melhor maneira que encontrei para obter essa funcionalidade é usar a funcionalidade da camada de automação incorporada ao Microsoft® Internet Explorer®. Com scripts do Microsoft Windows® escritos em PHP, você pode usar a interface Component Object Model (COM) para controlar o Internet Explorer para navegar entre páginas e, em seguida, usar métodos Document Object Model (DOM) para localizar páginas após executar ações específicas do usuário. .
Esta é a única maneira que conheço de testar a unidade do código JavaScript front-end. Admito que não é fácil escrever e manter, e esses testes são facilmente quebrados mesmo quando pequenas alterações são feitas na página.
Quais testes escrever e como escrevê-los
Ao escrever testes, gosto de cobrir os seguintes cenários:
Todos os testes positivos
Este conjunto de testes garante que tudo esteja funcionando como esperamos.
Todos os testes negativos
usam esses testes um por um para garantir que cada falha ou anomalia seja testada.
Teste de sequência positiva
Este conjunto de testes garante que as chamadas na sequência correta funcionem conforme esperado.
Teste de sequência negativa
Este conjunto de testes garante que as chamadas falhem quando não forem feitas na ordem correta.
Teste de Carga
Quando apropriado, um pequeno conjunto de testes pode ser realizado para determinar se o desempenho desses testes está dentro das nossas expectativas. Por exemplo, 2.000 chamadas devem ser concluídas em 2 segundos.
Testes de recursos
Esses testes garantem que a interface de programação de aplicativos (API) esteja alocando e liberando recursos corretamente - por exemplo, chamando a API baseada em arquivo de abertura, gravação e fechamento várias vezes seguidas para garantir que nenhum arquivo ainda esteja aberto.
Testes de retorno de chamada
Para APIs que possuem métodos de retorno de chamada, esses testes garantem que o código seja executado normalmente se nenhuma função de retorno de chamada for definida. Além disso, esses testes também podem garantir que o código ainda possa ser executado normalmente quando funções de retorno de chamada são definidas, mas essas funções de retorno de chamada operam incorretamente ou geram exceções.
Aqui estão algumas idéias sobre testes unitários. Tenho algumas sugestões sobre como escrever testes unitários:
Não use dados aleatórios
.Embora gerar dados aleatórios em uma interface possa parecer uma boa ideia, queremos evitar fazer isso porque esses dados podem se tornar muito difíceis de depurar. Se os dados forem gerados aleatoriamente em cada chamada, pode acontecer que ocorra um erro em um teste, mas não em outro. Se o seu teste exigir dados aleatórios, você poderá gerá-los em um arquivo e usá-lo sempre que executá-lo. Com essa abordagem, obtemos alguns dados "ruidosos", mas ainda podemos depurar erros.
Testes em grupo
Podemos facilmente acumular milhares de testes que levam várias horas para serem executados. Não há nada de errado com isso, mas agrupar esses testes nos permite executar rapidamente um conjunto de testes e verificar se há problemas importantes e, em seguida, executar o conjunto completo de testes à noite.
Escrevendo APIs e testes robustos
É importante escrever APIs e testes para que eles não quebrem facilmente quando novas funcionalidades forem adicionadas ou funcionalidades existentes forem modificadas. Não existe uma solução mágica universal, mas uma regra prática é que os testes que “oscilam” (falham às vezes, às vezes têm sucesso, repetidas vezes) devem ser descartados rapidamente.
Conclusão
Os testes unitários são de grande importância para os engenheiros. Eles são a base para o processo de desenvolvimento ágil (que dá grande ênfase à codificação porque a documentação exige alguma evidência de que o código está funcionando de acordo com as especificações). Os testes unitários fornecem essa evidência. O processo começa com testes unitários, que definem a funcionalidade que o código deveria implementar, mas atualmente não o faz. Portanto, todos os testes falharão inicialmente. Então, quando o código estiver quase completo, os testes serão aprovados. Quando todos os testes passam, o código fica muito completo.
Nunca escrevi código grande ou modifiquei um bloco de código grande ou complexo sem usar testes de unidade. Normalmente escrevo testes de unidade para o código existente antes de modificá-lo, apenas para ter certeza de que sei o que estou quebrando (ou não) quando modifico o código. Isso me dá muita confiança de que o código que forneço aos meus clientes está funcionando corretamente - mesmo às 3 da manhã.