Testes unitários
Testes unitários são uma categoria de testes de software onde o sistema é dividido em unidades, que são testadas individualmente.
O que seriam essas unidades? Normalmente são os métodos das nossas classes.
Durante os testes, cada unidade é exercitada de uma forma pré-determinada e então seus resultados são comparados com os resultados esperados. Se os resultados esperados forem iguais aos resultados reais, o teste passou. Se forem diferentes, o teste não passou. É um conceito muito simples, mas incrivelmente poderoso!
Existem algumas caracteristicas que devemos buscar alcançar em nossos testes unitarios:
- Eles devem funcionar sem intervenção manual. Não é bom se nossos testes necessitam de informações dadas pelo usuário, ou se dependem de alguem para avaliar seus resultados. O problema é que normalmente operações manuais são mais demoradas e propensas a erro. Além disso, se os testes rodam de forma automatica, podemos por exemplo montar um ambiente de integração contínua, que é muito interessante.
- Eles devem funcionar de forma independente uns dos outros. Se um teste depende de outro para funcionar, um dia ele pode falhar por que você esqueceu de rodar algum teste necessario e você ficará horas debugando seu código fonte achando que há um bug no seu sistema. Para alcançar esse efeito, é importante que cada teste “limpe a casa” após o trabalho, ou seja, liberar recursos alocados, etc. Em outras palavras: Um teste nunca deve deixar rastros. Para conseguir isso, as vezes é necessario um pouco de cuidado no design do nosso código: Evite o uso de singletons, variáveis globais, variáveis de classe, etc. E use injeção de dependencias.
- Testes devem ser simples de executar. Deve ser algo que possa ser feito com apenas um ou dois comandos simples. Se a tarefa de executar os testes for complicada, os programadores ficarão com preguiça de executa-los e aí nosso código fica desprotegido.
- Testes unitários devem ser simples de escrever e de entender. Se você tem muito trabalho para escrever um teste, pode ser que haja algum problema no design do seu código. O ideal é escrever o código de produção e de testes paralelamente. Dessa forma, você garante que o seu código é simples de usar e testar, pois se você identificar alguma complicação poderá corrigir imediatamente. Um redesign no código seria muito mais trabalhoso se os testes fossem escritos apenas no final do processo.
- Os testes devem ser determinísticos. Isto é: Toda vez que os testes forem executados, o resultado deve ser exatamente o mesmo. Para alcançar isso, devemos organizar nosso código de forma a ficar desacoplado de recursos externos.
Ao encontrar um bug no seu código, antes de corrigir primeiro você deve criar um teste que exponha esse bug. Tendo um teste para o bug, você garante que o mesmo não será reintroduzido no futuro. Escrever o teste antes garante que o teste que você escreveu está de fato exercitando aquele bug, pois escrevendo o teste após a correção, não há garantias de que você está de fato testando a condição que estava gerando o problema.
Ao escrever os testes, procure exercitar as situações mais problematicas. Não se restrinja aos casos mais simples. Exemlo: Se você está testando uma classe que processa uma lista de dados e retorna o menor e o maior elemento, é extremamente interessante que você teste os seguintes cenários:
- Lista vazia
- Lista com apenas um elemento
Não perca tempo testando código onde é improvavel que ocorra uma falha. Por exemplo, setters e getters de propriedades, que não fazem nada além de ler e gravar uma variável. Testar esse tipo de coisa tornará a atividade de escrever testes tediosa, e os programadores se sentirão desestimulados a escreve-los. Se (note o negrito no “Se”) um dia surgir um bug relacionado a estes trechos de código não testados, aí sim você escreve um teste para capturar o bug. Mas, pense comigo, qual a probabilidade de o sistema ter um bug por causa de um método cuja única função é pegar um parametro e copia-lo para uma variável de instância? É tão baixa que nem compensa o tempo gasto para escrever os testes.
Eu já vi casos de empresas que configuraram um ambiente de integração contínua ajustado para falhar o build caso a cobertura de código (code coverage – a quantidade de código exercitada pelos testes. Existem ferramentas para fazer essa medição) não atinja um determinado percentual. Isso é, na minha opinião, algo sem sentido. Uma decisão sem muito fundamento. Tal prática só iria encorajar os desenvolvedores a criarem testes inúteis para trechos de código que não necessitam de testes (conforme expliquei no parágrafo anterior).
Como eu disse antes, cada unidade deve ser testada individualmente, e de forma determinística. Imaginemos a seguinte situação:
Você precisa desenvolver uma classe que faça a leitura da temperatura ambiente e dispare um alarme caso a temperatura esteja maior que um certo limite. Para isso, existem dois equipamentos que devem ficar acoplados ao computador e são controlados por você: Um termometro e uma sirene.
Sua classe poderia ser mais ou menos assim:
class VerificadorDeTemperatura {
private float getTemperaturaAmbiente() {
/* Este método se comunica DIRETAMENTE com o dispositivo termometro e obtem a temperatura atual */
}
private void soarSirene() {
/* Este método se comunica DIRETAMENE com o dispositivo sirene e faz ela disparar */
}
public void verificarTemperatura(float limite) {
if (getTemperaturaAmbiente() > limite)
soarSirene();
}
}
Você consegue escrever os testes para essa classe seguindo as recomendações que eu listei acima? Não será possível executar os testes de forma determinística pois não temos controle sobre a temperatura retornada pelo termometro. Tambem não será possivel testar sem intervenção humana pois é necessário alguem para verificar se a sirene soou ou não a cada teste.
Devemos desacoplar a classe VerificadorDeTemperatura e os dispositivos físicos. Devemos criar interfaces para abstrair termometro e sirene. Desta forma, podemos usar implementações falsas destas interfaces nos testes, implementações estas que estão sob nosso inteiro controle. São os chamados test-doubles. O código ficaria assim:
interface Termometro {
float getTemperaturaAmbiente();
}
interface Sirene {
void soarSirene();
}
/* Omitirei as implementações destas duas interfaces, mas considere que são implementadas pelas classes TermometroImpl e SireneImpl, que fazem a comunicação com os dispositivos físicos */
class VerificadorDeTemperatura {
private Sirene sirene = new SireneImpl();
private Termometro termometro = new TermometroImpl();
public void verificarTemperatura(float limite) {
if (termometro.getTemperaturaAmbiente() > limite)
sirene.soarSirene();
}
}
Melhor assim? Não! Estamos no caminho certo, mas perceba que nossa classe VerificadorDeTemperatura ainda está fortemente acoplada aos dispositivos físicos. Ainda não temos como controla-los durante os testes pois a própria classe obtem as instancias das interfaces Sirene e Termometro.
Devemos aplicar injeção de dependencias, ou seja, a classe VerificadorDeTemperatura não mais busca as instancias de Sirene e Termometro, ao invés disso, as instancias são fornecidas a ela. O código ficaria assim:
interface Termometro {
float getTemperaturaAmbiente();
}
interface Sirene {
void soarSirene();
}
class VerificadorDeTemperatura {
private Sirene sirene;
private Termometro termometro;
public VerificadorDeTemperatura(Sirene sirene, Termometro termometro) {
this.sirene = sirene;
this.termometro = termometro;
}
public void verificarTemperatura(float limite) {
if (termometro.getTemperaturaAmbiente() > limite)
sirene.soarSirene();
}
}
Agora está bem melhor. Durante os testes, podemos ter algo como:
class SireneFalsa implements Sirene {
private boolean soouSirene;
public boolean getSoouSirene() {
return soouSirene;
}
public void soarSirene() {
soouSirene = true;
}
}
class TermometroFalso implements Termometro {
private float temperaturaAmbiente;
public void setTemperaturaAmbiente(float temperaturaAmbiente) {
this.temperaturaAmbiente = temperaturaAmbiente;
}
public float getTemperaturaAmbiente() {
return temperaturaAmbiente;
}
}
/* Esta é a classe que executará os testes */
public class TestVerificadorDeTemperatura {
public void testVerificarTemperatura() {
SireneFalsa sirene = new SireneFalsa();
TermometroFalso termometro = new TermometroFalso();
VerificadorDeTemperatura verificador = new VerificadorDeTemperatura(sirene, termometro);
termometro.setTemperaturaAmbiente(50);
verificador.verificarTemperatura(40);
if (!sirene.getSoouSirene())
System.out.println("Teste falhou! A sirene deveria soar");
}
}
Claro, isto não é um teste completo. Falta testar mais condições, e falta utilizar algum framework de testes (como o JUnit) de forma a poder automatizar os testes, mas eu só queria mostrar a idéia referente ao design propício à testabilidade.
Agora imagine se você primeiro escreve toda a classe e só depois vai testar. Talvez você teria criado algo como a primeira versão que eu mostrei aqui, e depois descobriria que é impossível de testar esta classe satisfatoriamente, e então terá o maior trabalho para refazer certas coisas e ter algo testavel. Se fossse uma classe muito complexa, talvez você acabe fazendo alguma gambiarra para testar sem ter que mexer muito na classe, o que no futuro poderia acabar se tornando um problema. Ou pior: Diante de tanto trabalho que teria para corrigir a classe, alegaria que está sem tempo e deixaria sem testes mesmo.


[...] fato, singletons, dependendo da maneira como são utilizados, podem gerar problemas com testes unitários, pois você não conseguirá alcançar o efeito de não deixar rastros do seus testes. Também não [...]
[...] etc. Para acelerar esse processo, testes bem escritos ajudam e muito: Por exemplo, use e abuse de test-doubles para evitar acessar recursos custosos durante os testes unitários, pois isso melhora a [...]