Esse artigo faz parte da revista Java Magazine edição 32. Clique aqui para ler todos os artigos desta edição

ht=34 alt=imagem_pdf.jpg src="/imagens/imagem_pdf.jpg" width=34 border=0>

Desenvolvimento Orientado a Testes

Dos Testes à Implementação, Passo a Passo e sem Receios

Conheça a estratégia de testes usada em processos ágeis como o Extreme Programming, que aumenta a qualidade do código e a produtividade e segurança do desenvolvedor

Vinícius Manhães Teles

Uma pesquisa do Departamento de Comércio dos EUA revelou que falhas de projetos de software são tão comuns e danosas que causam um prejuízo anual de mais de 60 bilhões de dólares à economia americana. O estudo também mostra que, embora não seja viável remover todos os erros, mais de um terço do prejuízo poderia ser eliminado se fosse usada uma infra-estrutura de testes melhor, permitindo identificar e remover defeitos antecipadamente de forma mais eficaz. Hoje se calcula que cerca de 50% dos defeitos são encontrados tardiamente – nas fases finais do desenvolvimento ou depois de os sistemas entrarem em produção, justamente quando a correção é mais cara.

Esse artigo apresenta a técnica de desenvolvimento orientado a testes, que tem como principal objetivo antecipar a identificação e correção de falhas no desenvolvimento, e é adotada em muitos processos ágeis de desenvolvimento, como o Extreme Programming (XP). Será utilizado um exemplo prático para demonstrar o uso da técnica, que é conhecida em inglês como Test-Driven Development – TDD.

Começando o exemplo

Nosso exemplo será um programa para geração de números primos, utilizando um conhecido algoritmo criado pelo matemático grego Erastóstenes. Não é necessário compreender o algoritmo para acompanhar o artigo (mas o leitor com inclinações matemáticas pode conferir o quadro “Como funciona o Crivo de Eratóstenes”). Basta entender que o resultado esperado é uma seqüência de números primos até um determinado valor N – por exemplo 2, 3, 5, 7 e 11, para N=11.

Para a construção dos testes será utilizado o popular framework JUnit, que já vem integrado em vários IDEs Java, entre eles Eclipse, IntelliJ, NetBeans e JBuilder. Neste artigo, utilizamos o Eclipse, mas os passos serão similares em outros IDEs, já que estamos usando apenas recursos do JUnit (você pode também usar seu editor de textos favorito e executar o JUnit diretamente, ou ainda usar uma ferramenta de automação como o Ant ou Maven).

Nosso ponto de partida será a classe de teste apresentada na Listagem 1, que faz uso de uma classe ainda não criada, chamada GeradorPrimos. Com isso, o teste se torna uma “especificação” de como a classe deverá funcionar quando estiver pronta. No nosso caso, espera-se que a classe gere um string, com uma lista de números primos separados por vírgula, menores ou iguais ao valor passado como argumento. Por exemplo, se buscássemos números primos até 10, teríamos como resultado a string "2, 3, 5, 7".

Para acompanhar os exemplos, passo a passo, defina um novo projeto e crie a classe testes GeradorPrimosTeste (veja o quadro "Primeiros Passos").

A chamada assertEquals(“2”, geradorPrimos.gerarPrimosAte(2)) no primeiro método de teste verifica se a classe testada é capaz de gerar primos corretamente até o valor máximo 2. É um caso trivial – afinal estamos só no início. Sabemos que o único número primo até 2 é o próprio 2, que é a resposta esperada para esse caso.

O código da classe de teste tem erros, pois não existe ainda a classe sendo testada, e o compilador vai considerar inválida a chamada ao método geradorPrimos.gerarPrimosAte(). Isso é normal quando escrevemos testes primeiro, antes do próprio código da aplicação. Apesar disso, neste ponto já temos definida estrutura inicial da classe GeradorPrimos, baseando-se no seu uso nos testes.

Vamos criar uma primeira versão da classe, escrita da forma mais simples possível. O código da Listagem 2 é suficiente para que o teste compile, mas isso ainda não significa que a classe passará no teste. Execute o teste no JUnit. Se o teste passar, o JUnit vai apresentar uma barra verde indicando que tudo correu bem. Se falhar, apresentará uma barra vermelha e uma mensagem indicando qual dos testes falhou. Podemos ver isso executando o JUnit com o que temos até o momento.

O JUnit mostra a barra vermelha como mostrado na Figura 1. Isso era esperado; afinal ainda não escrevemos a implementação correta do método que gera os números primos. Quando se programa utilizando TDD, é importante ter certeza de que o teste realmente será capaz de capturar um erro. Por isso sempre começamos inserindo um erro no código e conferindo se o teste falhou.

Em seguida, para verificar se o teste detecta o funcionamento correto da classe, fazemos outra implementação simples (e temporária) do método gerarPrimosAte(), retornando a string "2":

 

public String gerarPrimosAte(int i) {

  return "2";

}

 

Executando o teste novamente, o JUnit mostra que tudo correu bem, como pode ser observado na Figura 2.

Nossa primeira preocupação é escrever o teste e assegurar que ele funcione corretamente. Para fazer isso com segurança, é necessário certificar-se de que o teste falha quando temos absoluta certeza de que deveria falhar – e que passa quanto temos total confiança de que deveria passar. Descobrimos isso começando por soluções obviamente simples que quebrem o teste ou o façam funcionar. Depois disso, com a segurança de que o teste está correto, podemos cuidar da implementação real da classe, com a tranqüilidade de que o teste irá acusar falhas ou sucessos de forma coerente.

Esse é um conceito muito utilizado no Extreme Programming (XP), e conhecido em inglês pelo termo "baby steps" – avançar cuidadosamente dando um pequeno passo seguro de cada vez e só passando à atividade seguinte quando há certeza de que a atividade atual está 100% em ordem.

Incrementando os testes

Para continuar, poderíamos simplesmente escrever o restante do algoritmo, mas a verdade é que a classe GeradorPrimos já produz respostas certas para os testes que temos no momento. Será melhor criar novos cenários que levem à necessidade de estender a implementação da classe. Por exemplo (seguindo à risca o princípio dos baby steps), será que a classe conseguirá gerar números primos até 3? Eis um teste para descobrirmos isso:

 

public void testePrimosGeradosAteNumeroTres() throws Exception {

  GeradorPrimos geradorPrimos = new GeradorPrimos();

  assertEquals("2, 3", geradorPrimos.gerarPrimosAte(3));

}

 

Executando o teste no JUnit, confirmamos que ele falha, como podemos ver na Figura 4. Agora vamos forçar o teste a funcionar com a implementação mais simples possível. Veja a Listagem 3. O teste passa, mas há várias questões. Primeiro o código ainda simplifica demais o problema, pois será difícil gerar respostas para outros números, seguindo a linha de raciocínio adotada. Segundo, a variável i não expressa bem a sua intenção; e finalmente o código de teste apresenta uma incômoda duplicação. Para perceber os problemas, veja o código completo da classe de teste na Listagem 4.

A variável geradorPrimos é instanciada da mesma forma nos dois métodos de teste e a estrutura do assertEquals() é basicamente a mesma, mudando apenas os parâmetros. Isso fere um importante princípio, conhecido pela sigla DRY, do inglês "Don't Repeat Yourself" (não se repita). É importante eliminar duplicações para tornar o código mais claro e mais fácil de manter. Antes de prosseguir com o desenvolvimento, faremos algumas refatorações simples, começando por eliminar as repetições. 

Refatorar é uma prática comum em Extreme Programming (e em outros processos ágeis). Significa, basicamente, alterar o código sem alterar o que faz, e melhorar a estrutura do código, tornando-o mais simples, mais legível e mais fácil de manter. Uma das razões mais comuns para refatorar é a presença de repetições de código, as quais complicam o trabalho de manutenção e elevam o potencial de erros, além de aumentar o acoplamento com outras partes do código.

Para retirar a duplicação, vamos extrair um método. Nesta refatoração, isolamos em um método um trecho do código que se repete, e passamos a chamar este método nos locais onde o trecho era utilizado. O novo método é verificaPrimosGerados(), que você encontra na Listagem 5. Ele recebe como parâmetro a lista de primos esperada e o número máximo até o qual se deve procurar por primos.

Outra mudança foi renomear a variável i no código de geração para valorMaximo. Note ainda que o número 2 se comporta como um "número mágico": apenas lendo-o não temos como saber qual é o seu significado exato no programa. Literais espalhados pelo código freqüentemente têm essa característica: quem escreve o código é capaz de compreendê-los (pelo menos logo depois de escrever), mas outros programadores não entendem seu significado rapidamente, o que prejudica a manutenção. Resolvemos isso introduzindo uma constante, cujo nome expressa o significado do número. Veja a Listagem 6.

Testando entradas inválidas

Agora que o código de teste e o código do gerador de primos estão mais organizados, podemos avançar com segurança na solução do problema. Antes de verificar o que acontece ao tentar gerar primos até 4, percebemos que não existem testes para o caso de usuários fornecerem como valor máximo um número menor que 2.

O comportamento que desejamos para esses casos é que o método lance uma exceção ValorMaximoInvalidoException. Utilizamos um novo teste para expressar esse comportamento, como demonstrado na Listagem 7. A estrutura usada no método testeSeRejeitaValorMaximoUm() é utilizada tipicamente quando se deseja assegurar que um código lança uma exceção em certas circunstâncias.

Analisando o processamento do método, note que, se o gerador de primos estiver funcionando corretamente, a chamada geradorPrimos.gerarPrimosAte(1) irá lançar a exceção esperada e o processamento será desviado para o bloco catch. Lá, a instrução assertTrue(true) serve apenas para indicar, para quem está lendo o código, que alcançar o bloco catch é o comportamento esperado para o teste. 

Se a exceção não for lançada, a linha contendo a instrução fail() será executada, forçando o JUnit a apresentar uma falha com a descrição passada ao método fail(). Inicialmente o código contém erros, pois a classe ValorMaximoInvalidoException ainda não existe. Vamos criá-la:

 

public class ValorMaximoInvalidoException extends Exception {

   public ValorMaximoInvalidoException() {

     super("O valor maximo deve ser maior ou igual a 2");

   }

 }

 

Finalmente, para que o código compile, é necessário que o gerador de primos declare lançar a nova  exceção:

 

public String gerarPrimosAte(int valorMaximo)

      throws ValorMaximoInvalidoException { ...

 

Agora o teste compila, e gera a falha abaixo quando executado:

 

testeSeRejeitaValorMaximoUm()

    junit.framework.AssertionFailedError:

         Deveria ter lancado ValorMaximoInvalidoException

 

Como temos feito até aqui, primeiro esperamos que o teste falhe, depois o fazemos passar. Fazendo a correção apresentada na Listagem 8, o teste funciona.

Refatorando os testes

O novo teste introduzido na Listagem 7 duplicou a instanciação da variável geradorPrimos. Para evitar esse problema, refatoramos o teste, tornando essa variável um atributo da classe. Você pode verificar as mudanças comparando a Listagem 9 com as Listagens 5 e 7.

Analisando a classe GeradorPrimos, notamos que a legibilidade do método gerarPrimosAte() foi  prejudicada, porque ele cuida primeiro da exceção, e só depois do processamento do roteiro natural (caso o parâmetro de entrada fosse válido). É recomendável que os métodos primeiro cuidem do roteiro natural e depois tratem os casos excepcionais. Resolvemos isso com a refatoração indicada na ...

Quer ler esse conteúdo completo? Tenha acesso completo