Analisando os 15 anos de PHP, vemos que ele cresceu de uma simples linguagem de script dinâmica, alternativa aos scripts CGI populares naquele período, para a linguagem de programação completa que é hoje. À medida que a base de código cresce, o teste manual se torna uma tarefa impossível e cada mudança feita no código, grande ou pequena, pode afetar todo o aplicativo. Os efeitos podem ser uma página que não carrega ou um formulário que não salva ou também pode ser algo difícil de detectar ou que apareça apenas sob determinadas circunstâncias. Pode até mesmo fazer com que um problema anterior reapareça no aplicativo. Diversas ferramentas de teste foram desenvolvidas para resolver esses problemas.
Um método popular é conhecido como teste funcional ou de aceitação e testa o aplicativo por meio da interação típica do usuário do aplicativo. Essa é uma boa técnica para testar os diversos processos no aplicativo, mas pode ser um processo bastante lento e geralmente não realiza um trabalho tão bom em testar as classes e funções de nível inferior para se certificar de que estejam funcionando conforme o pretendido. É ai que outro método de teste, o teste de unidade, entra em cena. A meta é testar a funcionalidade do código subjacente do aplicativo, a fim de garantir que os resultados corretos sejam fornecidos na execução. Frequentemente, esses aplicativos da Web "crescidos" recebem muito código herdado que, com o tempo, dificultam a realização de testes, o que reduz a capacidade das equipes de desenvolvimento de fornecer uma boa cobertura de teste para um aplicativo. Isso é chamado comumente de "código intestável". Vamos ver como identificar isso em seu aplicativo e como corrigir.
Identificando o código intestável
As áreas problemáticas de sua base de código que não podem ser testadas normalmente não são aparentes quando o código está sendo escrito. Quando você escreve um código para um aplicativo PHP, há uma tendência de personalizá-lo da forma que a solicitação da Web flui, usando uma abordagem mais voltada aos procedimentos no design do aplicativo. Uma urgência de terminar o projeto ou corrigir o aplicativo de forma apressada pode fazer com que os desenvolvedores "peguem atalhos" a fim de completar o código rapidamente. Antes, um código confuso ou escrito de maneira inadequada podia compor os problemas de impossibilidade de teste, pois os desenvolvedores normalmente tentavam realizar a correção menos arriscada possível, mesmo que isso significasse problemas de suporte no decorrer do tempo. Todos esses problemas são bastante intestáveis pelo teste de unidade sem correr riscos.
Funções que dependem do estado global
Variáveis globais são uma conveniência nos aplicativos PHP. Elas permitem que você tenha variáveis ou objetos que podem ser inicializados logo no início em seu aplicativo e podem ser aproveitados em qualquer local do aplicativo. No entanto, essa flexibilidade tem um custo, pois o uso pesado de variáveis globais é um problema comum visto em códigos intestáveis. Podemos ver isso na Listagem 1.
Listagem 1. Função que depende do estado global
<?php
function formatNumber($number)
{
global $decimal_precision, $decimal_separator, $thousands_separator;
if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ',';
return number_format($number, $decimal_precision, $decimal_separator,
$thousands_separator);
}
|
Dois problemas diferentes ocorrem devido a essas variáveis globais. O primeiro problema é que você precisa dar conta de cada uma delas em seu teste, se certificando de que as configura com valores válidos esperados pela função. O segundo e maior problema é que se você não mudar o estado nos testes subsequentes e invalidar seus resultados, precisará redefinir o estado global de volta ao estado que estava antes da execução do teste. O PHPUnit tem recursos que podem fazer o backup de variáveis globais e restaurá-las após a execução do teste, o que pode ajudar a reduzir o problema. No entanto, é melhor fornecer uma maneira de a classe do testador passar diretamente para essas variáveis globais os valores que possam ser usados pelo método. A Listagem 2 mostra um exemplo de como fazer isso.
Listagem 2. Função corrigida para permitir a substituição das variáveis globais
<?php
function formatNumber($number, $decimal_precision = null, $decimal_separator = null,
$thousands_separator = null)
{
if ( is_null($decimal_precision) ) global $decimal_precision;
if ( is_null($decimal_separator) ) global $decimal_separator;
if ( is_null($thousands_separator) ) global $thousands_separator;
if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ',';
return number_format($number, $decimal_precision, $decimal_separator,
$thousands_separator);
}
|
Essa ação não apenas tornou o código mais testável, mas também fez com que ele não dependa das variáveis globais no método. Isso pode abrir a possibilidade de refatoração desse código a fim de não usar as variáveis globais de jeito nenhum.
Singletons que não podem ser redefinidos
Singletons são classes projetadas para ter apenas uma instância existente por vez em um aplicativo. Eles são um padrão comum usado para objetos globais em um aplicativo, como conexões de banco de dados e definições de configurações. Eles são normalmente considerados um tabu em um aplicativo, o que muitos desenvolvedores consideram ser uma distinção injusta, devido à utilidade de sempre ter um objeto disponível para uso. Muito disso vem do uso exagerado de singletons, uma vez que muitos desses chamados objetos deuses não podem ser estendidos. Porém, de uma perspectiva de teste, um grande problema é que eles são frequentemente imutáveis. Vamos analisar a Listagem 3 como exemplo.
Listagem 3. Objeto singleton que queremos testar
<?php
class Singleton
{
private static $instance;
protected function __construct() { }
private final function __clone() {}
public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
}
return self::$instance;
}
}
|
Você pode ver que após o singleton ser instanciado pela primeira vez,
toda chamada feita para o método getInstance() retorna
o mesmo objeto e não um novo, o que pode se tornar um grande
problema se fizermos alterações nesse objeto. A solução mais fácil é adicionar
um método ao objeto que possa redefini-lo. A Listagem 4
mostra esse exemplo.
Listagem 4. Objeto singleton com um método de redefinição adicionado
<?php
class Singleton
{
private static $instance;
protected function __construct() { }
private final function __clone() {}
public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
}
return self::$instance;
}
public static function reset()
{
self::$instance = null;
}
}
|
Agora, podemos chamar o método de redefinição para iniciar cada teste a fim de garantir que estamos passando pelo código de inicialização para o objeto singleton em cada teste. Ter esse método disponível pode ser útil no aplicativo em geral, pois agora o singleton se torna facilmente mutável.
Trabalhando no construtor de classe
Uma boa prática para o teste de unidade é testar apenas o que você pretende, e evitar ter que configurar mais objetos e variáveis do que você precisa. Cada objeto e variável definidos também precisam ser removidos após o fato. Isso se torna um problema para itens mais incômodos como arquivos e tabelas de banco de dados, nos quais se você precisa modificar o estado, precisa ter extremo cuidado para limpar tudo após a conclusão do teste. A maior barreira para manter essa regra intacta é o construtor do próprio objeto, que faz todo tipo de coisa que não diz respeito ao seu teste. Considere a Listagem 5 como exemplo.
Listagem 5. Classe com um método de singleton grande
<?php
class MyClass
{
protected $results;
public function __construct()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
}
public function getFirstResult()
{
return $this->results[0];
}
}
|
Aqui, para testar o método fdfdfd no objeto,
acabamos precisando configurar uma conexão do banco de dados, ter registros na
tabela e limpar todos esses recursos após o fato. Isso
parece exagerado quando nada disso é necessário para testar o método
fdfdfd . Portanto, vamos modificar o
construtor, como mostra a Listagem 6.
Listagem 6. Classe modificada para ignorar opcionalmente toda a lógica de inicialização desnecessária
<?php
class MyClass
{
protected $results;
public function __construct($init = true)
{
if ( $init ) $this->init();
}
public function init()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
}
public function getFirstResult()
{
return $this->results[0];
}
}
|
Refatoramos a grande quantidade de código no construtor para inserir um método
init() , que ainda será chamado por
padrão no construtor, a fim de evitar a quebra do código existente. No entanto,
agora podem apenas passar um operador booleano falso para o construtor durante nossos testes
para evitar chamar o método init() e toda a
lógica de inicialização desnecessária. Essa refatoramento da classe também melhora
o código para separarmos o código de inicialização do código de construção do
objeto.
Ter dependências de classe codificadas
Como vimos na seção anterior, um grande problema no design da classe que dificulta o teste é ter que inicializar todos os tipos de objetos que não são necessários para seu teste. Anteriormente, vimos como uma lógica pesada de inicialização pode adicionar todos os tipos de sobrecarga ao escrever um teste (especialmente quando o teste não precisa de nada disso para ter êxito), mas outro problema pode ocorrer quando criamos diretamente novos objetos dentro dos métodos da classe que estamos testando. Vamos analisar a Listagem 7 para obter um exemplo desse código problemático.
Listagem 7. Classe com um método que inicializa diretamente outro objeto
<?php
class MyUserClass
{
public function getUserList()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$results = $dbconn->query('select name from user');
sort($results);
return $results;
}
}
|
vamos supor que estamos testando o método getUserList acima,
mas o foco de nosso teste é assegurar que a lista de usuários retornada
seja classificada em ordem alfabética. Nesse caso, o fato de podermos
obter os registros do banco de dados realmente não importa, já que o que
estamos testando é nossa capacidade de classificar os registros retornados. O
problema é que, como nós instanciamos diretamente um objeto de conexão de banco de dados dentro do método, precisamos realizar todo esse trabalho de construção para testar
adequadamente o método. Portanto, vamos fazer uma alteração para permitir
a interjeição de um objeto, como mostra a Listagem 8.
Listagem 8. Classe com um método que inicializa diretamente outro objeto, mas também fornece uma maneira de substituí-lo
<?php
class MyUserClass
{
public function getUserList($dbconn = null)
{
if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) {
$dbconn = new DatabaseConnection('localhost','user','password');
}
$results = $dbconn->query('select name from user');
sort($results);
return $results;
}
}
|
Agora, você pode passar diretamente um objeto compatível com o objeto de conexão de banco de dados esperado e ele usará esse objeto em vez de criar um novo. O objeto passado pode simplesmente ser um objeto mock, ou seja, um objeto no qual codificamos vários dos valores de retorno dos métodos chamados para fornecer diretamente os dados que desejamos usar. Nesse caso, nós substituiríamos o método de consulta do objeto de conexão do banco de dados para podermos retornar apenas os resultados em vez de recorrer ao banco de dados para obtê-los. Fazer esse tipo de refatoração também pode melhorar o método, permitindo que seu aplicativo realize a interjeição de diferentes conexões de banco de dados em vez de ficar vinculado apenas ao padrão especificado.
É claro que escrever um código mais testável tem o benefício óbvio de facilitar a geração de testes unitários para seu aplicativo PHP (que você viu nos exemplos apresentados neste artigo), mas isso também cria um aplicativo com um design melhor, mais modular e mais estável durante o processo. Todos nós vimos diversos níveis de códigos "espaguete" que entrelaçam de forma sólida os negócios e exibem lógica em uma grande bagunça processual em aplicativos PHP e que, sem dúvida, podem causar pesadelos de suporte para qualquer pessoa que precisar analisá-los. Durante o processo de tornar o código testável, nós refatoramos um código anteriormente problemático, não apenas em seu design, mas também em suas funções. Abrimos opções para uma melhor reutilização do código, fazendo com que as funções e classes não tivessem apenas um propósito e as tornando mais úteis por outras áreas do aplicativo, removendo dependências codificadas. Além disso, facilitamos o trabalho de suporte da base de código removendo o código de qualidade inferior e substituindo por outro com uma qualidade muito melhor.
Neste artigo, analisamos como tornar o código PHP mais testável por meio de vários exemplos de códigos intestáveis clássicos nos aplicativos PHP. Exploramos como as situações surgem no aplicativo e vimos como corrigir da melhor maneira o código problemático para possibilitar o teste. Também vimos como essas mudanças em seu código não apenas tornaram o código mais testável, mas também melhoraram a qualidade do código em geral e promoveram a reutilização do código nas seções de código refatoradas.
| Descrição | Nome | Tamanho | Método de download |
|---|---|---|---|
| Article source code | code_examples.zip | 3KB | HTTP |
Informações sobre métodos de download
Aprender
- A série de tutoriais do developerWorks "Learning PHP" o leva do script PHP mais básico até trabalhar com bancos de dados e fluir do sistema de arquivos.
-
PHP.net é o recurso central para desenvolvedores de PHP.
- Confira "Lista de leitura recomendada para PHP."
- Navegue em todo o conteúdo PHP no developerWorks.
- Expanda suas qualificações em PHP verificando os recursos do projeto de PHP do IBM developerWorks.
-
A PHP V5 migration guide (Jack Herrington, developerWorks,
setembro de 2006): Learn how to migrate code developed in PHP V4 to V5.
-
Planet PHP é a fonte de notícias da comunidade de desenvolvedores de PHP.
-
Safari bookstore: Visite essa biblioteca de referências eletrônicas para encontrar os recursos técnicos específicos.
- Siga o developerWorks no Twitter.
- Para ouvir entrevistas e discussões interessantes para desenvolvedores de software, confira os Podcasts do developerWorks.
-
eventos técnicos e webcasts do developerWorks: Permaneça atualizado com os webcasts e eventos técnicos do developerWorks.
Obter produtos e tecnologias
- Inove seu próximo projeto de desenvolvimento de software livre com a versão de teste do software IBM, disponível para download ou em DVD.
Discutir
- Participe dos blogs do developerWorks e participe da comunidade do developerWorks.

John Mertic é um engenheiro de software na SugarCRM, e tem vários anos de experiência com aplicativos PHP da Web. Na SugarCRM, ele especializou-se em arquitetura de interface com o usuário, móvel e integração de dados. Um ávido escritor, ele teve artigos publicados na php|architect, IBM developerWorks e na Apple Developer Connector, e é autor do livro "The Definitive Guide to SugarCRM: Better Business Applications". Ele também já fez contribuições a vários projetos de software livre, mais notavelmente ao projeto PHP onde ele é o criador e mantenedor do PHP Windows Installer. Seu e-mail de contato é jmertic@gmail.com.