|
|
Daniel Quirino Oliveira
Não importa qual tecnologia você pretende usar: seja Java ou .NET, sem testes não há como garantir software de boa qualidade.
O que me motivou a escrever este texto foi um anúncio publicitário da Rational. Em seu texto havia um pequeno teste, que dizia mais ou menos assim:
"Testes devem ser feitos:
no início
durante
em pessoas que acham que testes só devem ser feitos no final"
Eu, indignado, perguntei: "quem será que só aplica teste sobre o software no final do processo de desenvolvimento?". Aí obtive a resposta de uma voz interna: "Todo mundo, inclusive eu!".
Sim. Embora muitos de nós, programadores, saibamos que testes devem ser feitos antes, durante e depois do desenvolvimento, poucos de nós aplicamos isso no dia-a-dia. Por quê? Porque testes são chatos de serem feitos, codificar é muito mais prazeroso.
Mas este prazer traz responsabilidades. Está nas mãos dos programadores grande parte do sucesso e da qualidade do sistema: afinal, somos nós que transformamos toda aquela especificação proveniente da análise de requisitos em software efetivamente.
Mas voltando ao teste da Rational, o pessoal de engenharia de software diria que a resposta certa é, não só durante, mas sempre! Deveríamos fazer testes durante todo o processo de desenvolvimento, desde quando começamos a análise e o projeto, passando pela implementação até chegar à integração dos módulos.
Exagero? Não. Se testassem as especificações antes mesmo de criarem um modelo, se testassem os modelos antes de entregarem aos programadores e, por fim, se testássemos antes mesmo de tentarmos integrar, a quantidade de bugs na versão final do software seria muito menor. E como resultado, menos recursos gastos com correção de bugs, usuários e clientes mais satisfeitos e programadores mais saudáveis (afinal, a pressão aumenta conforme a lista de bugs aumenta). Mas, se mesmo assim ainda não foi possível convencê-lo de que testar é importante, este artigo é para você.
3 motivos para se aplicar testes continuamente
Fazer testes é fundamental. Pensei em apenas 3 itens para tentar convencê-lo a dedicar um pouco mais do seu tempo com testes, mas poderia ter pensado em outras mais. Poderia ter feito um artigo com o nome "101 razões para se aplicar testes contínuos sobre o desenvolvimento de software", mas isso fica para uma próxima. Por enquanto vamos ficar com estas 3 razões.
1. Testes garantem sua vantagem competitiva:
Este recado serve para dois tipos de pessoas: gerentes e programadores. Gerentes subestimam a importância dos testes, criando cronogramas enxutíssimos para atender a vontade do cliente ou para chegar ao mercado antes da concorrência. O resultado disso é um produto final sem qualidade, pois não pôde ser avaliado durante todo o seu processo de criação. Conseqüências? Se o software não for confiável, a empresa vai ficar marcada como "uma empresa que não fabrica software de qualidade" e, adivinhem, a ela vai perder mercado.
Já os programadores que fazem testes freqüentemente tendem a entregar códigos menos problemáticos. Isso resulta numa melhor produtividade (afinal, o tempo perdido na correção de erros pode ser gasto na criação de mais códigos ou em mais testes!). E os chefes gostam de produtividade. Ou seja, testes garantem elogios, boas referências ou até mesmo uma promoção (com direito a aumento e tudo mais).
2. Testes economizam tempo e dinheiro:
Não pense em testes como tempo perdido. Pense em finais de semana que você não vai precisar passar depurando programas. Ou pense em clientes mais felizes que sempre vão associar o nome da sua empresa a palavras como "qualidade" e "confiança". E economiza-se em ambos os casos:
- não é preciso pagar hora-extra para os programadores acertarem problemas em horários heterodoxos;
- não é preciso gastar fortunas em publicidade para arrebanhar novos clientes para repôr aqueles que abandonaram sua empresa porque perderam a paciência com o software mal-testado.
3. Testes não são tão demorados de se fazer:
Há ferramentas que auxiliam e automatizam boa parte do processo de testes, gerando desde uma lista detalhada dos erros encontrados até relatórios completos sobre os testes. Para o mundo Java há o famoso JUnit e suas variações, (como HTTPUnit, Cactus, SwingUnit e SWTUnit), que oferecem um ambiente para criação e execução de testes de unidade. Para o mundo .NET há o NUnit, versão para a plataforma da Microsoft do JUnit.
Além do mais, teste é uma das bases para o XP, um modelo de desenvolvimento ágil de softwares. Mas se você ainda não se convenceu de que testes contínuos são importantes, então leia estes artigos http://brazil.joelonsoftware.com/Articles/TopFiveReasonsYouDontHave.html e http://www.onjava.com/pub/a/onjava/2003/04/02/javaxpckbk.html.
Mas, se você já estiver convencido de que testes são muito importantes mas ainda não sabe como aplicá-los e nem quais ferramentas usar, continue sentado.
Testes mais fáceis com JUnit
JUnit é um framework open-source, feito por Eric Gamma e Kent Beck (dois dos principais nomes por trás da metodologia XP), que lhe dá todo o ambiente necessário para que você possa testar seus códigos antes de promovê-los ao status de "stable release version". A maioria das IDEs (JBuilder, JDeveloper, Netbeans, Eclipse ...) incorporam o JUnit dentro de seu ambiente de desenvolvimento, facilitando ainda mais o seu uso.
Para instalá-lo, simplesmente descompacte o arquivo e adicione o arquivo junit.jar no seu classpath.
O JUnit apresenta três diferentes interfaces: uma interface texto (junit.textui.TestRunner), uma interface AWT (junit.awtui.TestRunner) e uma interface Swing (junit.swingui.TestRunner). Para executar qualquer uma delas, digite no console: java [interface] [classe de testes]. Exemplo: java junit.swingui.TestRunner MyTest.
E criar testes para o JUnit é muito simples. Para se testar uma classe específica, basta criar uma classe que deve ser herdeira da classe junit.framework.TestCase. Para fazer os testes dos métodos de uma certa classe, a classe de teste deve implementar um testXXX(), sendo XXX, normalmente (mas não obrigatoriamente), o nome do método da classe que você está testando.
Hmmm, ficou complicado, né? Então é melhor vermos um exemplo.
Conversor de temperatura
Para o exemplo, vamos implementar um sistema que faça conversão de temperaturas de Celsius para Fahrenheit e vice-versa. Lembrando os tempos de colégio e colando do livro "Fundamentos da Física 2" (Ramalho, Ferraro, Soares - Ed. Moderna), as fórmulas para conversão são as seguintes:
Celsius para Fahrenheit: Tf = 1,8 x Tc + 32
Fahrenheit para Celsius: Tc = (5 x Tf - 160)/9
Para nosso sistema de conversão, vamos precisar de uma interface para representar a entidade temperatura para nosso sistema (código 1), implementações das escalas Celsius e Fahrenheit e (códigos 2 e 3 respectivamente), claro, nosso conversor universal de temperaturas (código 4).
Depois disso, vamos montar uma classe que, usando o JUnit, vai testar o que pensamos que nossas classes deveria fazer. Então, vamos ao código:
01 public interface Temperature{
02 public double getValue();
03
04 public void setValue(double value) throws Exception;
05
06 public double getFREEZE();
07
08 public double getBOIL();
09
10 public double getZERO();
11 }
|
Classes que implementam Temperature:
01 public class CelsiusTemperature implements Temperature{
02
03 private double value;
04
05 private final double FREEZE = 0;
06
07 private final double BOIL = 100;
08
09 private final double ZERO = -273;
10
11 public CelsiusTemperature(){ }
12
13 public double getValue(){
14 return value;
15 }
16
17 public void setValue(double value) throws Exception{
18 if(value < ZERO) throw new Exception("Não há temperatura abaixo do zero absoluto");
19 else this.value = value;
20 }
21
22 public double getFREEZE(){ return FREEZE;}
23
24 public double getBOIL(){ return BOIL;}
25
26 public double getZERO(){ return ZERO;}
27
28 public String toString(){
29 return getValue()+" C";
30 }
31
32 public boolean equals(Object other){
33 if(other instanceof CelsiusTemperature)
34 return (other.getValue() == getValue());
35 else return false;
36 }
37 }
|
01 public class FahrenheitTemperature implements Temperature{
02
03 private double value;
04
05 private final double FREEZE = 32;
06
07 private final double BOIL = 212;
08
09 private final double ZERO = -459.4;
10
11 public FahrenheitTemperature(){ }
12
13 public double getValue(){
14 return value;
15 }
16
17 public void setValue(double value) throws Exception{
18 if(value < ZERO) throw new Exception("Não há temperatura abaixo do zero absoluto");
19 else this.value = value;
20 }
21
22 public double getFREEZE(){ return FREEZE;}
23
24 public double getBOIL(){ return BOIL;}
25
26 public double getZERO(){ return ZERO;}
27
28 public String toString(){
29 return getValue()+" F";
30 }
31
32 public boolean equals(Object other){
33 if(other instanceof FahrenheitTemperature)
34 return (other.getValue() == getValue());
35 else return false;
36 }
37 }
|
Segue o conversor de temperaturas. Será que ele está correto?
01 public class TemperatureTransformer{
02
03 public TemperatureTransformer(){ }
04
05 public Temperature convert(Temperature temp) throws Exception{
06 if(temp instanceof CelsiusTemperature) return convertToFahrenheit(temp);
07 else return convertToCelsius(temp);
08 }
09
10 private Temperature convertToFahrenheit(Temperature celsius) throws Exception{
11 FahrenheitTemperature f = new FahrenheitTemperature();
12 double cvalue = celsius.getValue();
13 double fvalue = 1.8*cvalue+f.getFREEZE(); // formulinha 1 :)
14 f.setValue(fvalue);
15 return f;
16 }
17
18 private Temperature convertToCelsius(Temperature fahrenheit) throws Exception{
19 CelsiusTemperature c = new CelsiusTemperature();
20 double fvalue = fahrenheit.getValue();
21 double cvalue = (5/9)*fvalue-5*fahrenheit.getFREEZE();// formulinha 2 ?!
22 c.setValue(cvalue);
23 return c;
24 }
25
26 }
|
A idéia de criar uma interface para temperatura e fazer as escalas celsius e fahrenheit a implementar ajuda a tornar o sistema um pouco mais flexível e desacoplado da implementação das minhas escalas.
No restante dos códigos, nada anormal. O sistema funciona da seguinte forma: o usuário instancia um bean de uma certa escala termométrica e passa esta instancia como parâmetro para o método convert() da classe TemperatureTransformer, que retorna uma instância de um Temperature.
Além disso, se o usuário tentar atribuir um valor que seja abaixo do zero absoluto, é jogada uma exceção avisando que tal valor não pode ser atribuido. Aparentemente, nada poderia dar errado aqui, certo? Errado. Veja que há um erro na implementação da fórmula para converter a temperatura para a escala Celsius. Ao invés do programador (eu, no caso) dividir toda a expressão por 9, ele, sem perceber, só divide o primeiro termo, podendo causar algumas dores de cabeça.
Ainda bem que o sistema converte apenas temperaturas, mas imagine se convertesse moedas! Um desastre. Então, para evitar dores de cabeça e desastres, vamos testar nosso sistema antes de promovê-lo a versão 1.0. Para tanto, temos que criar a tal classe de teste. O código está abaixo:
O Teste
01 import junit.framework.TestCase;
02
03 public class TemperatureTransformerTest extends TestCase{
04 public void testConvert() throws Exception{
05 Temperature t = new FahrenheitTemperature();
06 t.setValue(32);
07 TemperatureTransformer tc = new TemperatureTransformer();
08 Temperature f = tc.convert(t);
09 assertTrue(f.getValue() == 0);
10 }
11 }
|
1, 2... 12 linhas de código para criar um teste. Acho que isso não deve tomar muito tempo, certo?
Uma convenção normalmente usada é nomear a classe de testes com o nome da classe que vamos testar seguido da palavra Test. Ou seja, como vamos testar a classe TemperatureTransformer, então daremos o nome de TemperatureTransformerTest para a classe de testes. Mas, lembre-se: isso é apenas uma convenção. Nada lhe impede de dar os nomes que você quiser para sua classe. No entanto, o JUnit só executa testes sobre aqueles métodos que começarem com a palavra "test". E isso não é convenção, é obrigatório mesmo.
A parte mais importante desta classe está na linha 10. O método assertTrue(boolean) checa se determinada condição é verdadeira. E vai ser a partir da resposta deste método que o JUnit vai fazer a detecção de erros do seu código. Na nossa classe de testes, pediu-se para verificar se a conversão de 32 graus Fahrenheit para Celsius é igual a 0. Porém, como vimos, a fórmula de conversão foi implementada de maneira errada.
E, por causa disso, o JUnit vai acusar uma falha (junit.framework.AssertionFailedError).
Detectada a falha, podemos consertar nosso erro:
01 public class TemperatureTransformer{
02
03 public TemperatureTransformer(){ }
04
05 public Temperature convert(Temperature temp) throws Exception{
06 if(temp instanceof CelsiusTemperature) return convertToFahrenheit(temp);
07 else return convertToCelsius(temp);
08 }
09
10 private Temperature convertToFahrenheit(Temperature celsius) throws Exception{
11 FahrenheitTemperature f = new FahrenheitTemperature();
12 double cvalue = celsius.getValue();
13 double fvalue = 1.8*cvalue+f.getFREEZE(); // formulinha 1 :)
14 f.setValue(fvalue);
15 return f;
16 }
17
18 private Temperature convertToCelsius(Temperature fahrenheit) throws Exception{
19 CelsiusTemperature c = new CelsiusTemperature();
20 double fvalue = fahrenheit.getValue();
21 double cvalue = (5*fvalue-5*fahrenheit.getFREEZE())/9;// formulinha 2 :)
22 c.setValue(cvalue);
23 return c;
24 }
25
26 }
|
Se fizermos novamente os testes, não veremos mais as mensagens de erro.
E é isso. Como podemos ver, fazer testes não são tão doloroso nem tão demorados assim. Uma boa dica de como fazer testes é ensinada pela metodologia XP, que tem como um de seus pilares testes contínuos. E lembre-se: não acredite puramente nos seus dotes de programador; testes são as únicas formas de garantir que seu software faz exatamente aquilo que ele deveria fazer.
|
|
|