Conceitos: Teste do Desenvolvedor
Tópicos
Introdução
A frase "Teste do Desenvolvedor" é utilizada para categorizar as atividades
de teste executadas mais apropriadamente pelos desenvolvedores de software. Ela também inclui os artefatos criados por essas atividades.
O Teste do Desenvolvedor abrange o
trabalho tradicionalmente considerado sob as categorias a seguir: Teste de Unidade,
em grande parte do Teste de Integração e alguns aspectos do que é geralmente conhecido
como Teste do Sistema. Embora o Teste do Desenvolvedor esteja associado tradicionalmente a atividades da disciplina Implementação, ele também tem um relacionamento com atividades da disciplina Análise e Design.
Considerando o Teste do Desenvolvedor neste modo "holístico", você ajuda
a diminuir o risco associado à abordagem mais "atomística" adotada
tradicionalmente. Na abordagem tradicional do Teste do Desenvolvedor, o esforço inicial se concentra em avaliar se todas as unidades estão funcionando de forma independente.
Em uma etapa posterior do ciclo de vida de desenvolvimento, à medida que o trabalho de desenvolvimento chega ao fim, as unidades integradas são organizadas em um subsistema de trabalho ou sistema e testadas nesse cenário pela primeira vez.
Essa abordagem apresenta diversas falhas.
Em primeiro lugar, como estimula uma abordagem encenada do teste das unidades integradas e depois de subsistemas, todos os erros identificados durante esses testes costumam ser detectados tarde demais.
Essa descoberta tardia geralmente resulta na decisão de não executar nenhuma ação corretiva ou requer uma quantidade significativa de retrabalho para corrigir os erros.
O retrabalho é dispendioso e prejudica o progresso em outras áreas.
Isso aumenta o risco de o projeto sair do rumo determinado ou ser abandonado.
Em segundo lugar, a criação de limites rígidos entre os Testes da Unidade, de Integração e do Sistema aumenta a probabilidade de os erros fora dos limites não serem detectados por ninguém.
O risco é maior quando a responsabilidade por esses tipos de testes é atribuída à equipes distintas.
O estilo de teste do desenvolvedor recomendado pelo RUP estimula o desenvolvedor a se concentrar nos testes mais valiosos e apropriados a serem conduzidos no momento especificado.
Mesmo no escopo de uma única iteração, o desenvolvedor geralmente consegue ser mais eficiente se detectar e corrigir o máximo de defeitos possível no seu próprio código do que ter o trabalho adicional de entregar o teste para um grupo separado.
O resultado desejado é a descoberta antecipada dos erros de software mais significativos,
independentemente se os erros estão na unidade independente, na integração das
unidades ou no trabalho das unidades integradas em um cenário significativo do usuário
final.
Armadilhas Iniciais do Teste do
Desenvolvedor
Muitos desenvolvedores que tentam realizar um trabalho bem mais cuidadoso de teste desistem logo depois que começam essa tentativa.
Eles acham que o esforço parece não compensar.
Além disso, alguns desenvolvedores que começam bem no teste acham que criaram um conjunto de testes insustentável e acabam desistindo.
Esta página fornece algumas diretrizes para superar os primeiros obstáculos e criar um conjunto de testes que evite a armadilha da manutenibilidade.
Para obter informações adicionais,
consulte Diretrizes: Mantendo Conjuntos de Testes Automatizados.
Estabelecer Expectativas 
Os indivíduos que vêem recompensas no teste do desenvolvedor o executam.
Aqueles que o consideram uma tarefa enfadonha encontram formas de evitá-lo.
Essa atitude faz parte simplesmente da natureza da maioria dos desenvolvedores em grande parte dos ramos, e tratá-la como uma falta de disciplina vergonhosa não tem funcionado historicamente.
Assim, como desenvolvedor, convém esperar que o teste traga recompensas e fazer o que for necessário para torná-lo recompensador.
O teste do desenvolvedor ideal segue um ciclo de edição-teste bastante restrito.
Você efetua uma pequena mudança no produto, como adicionar um novo método a uma classe, e reexecuta os testes imediatamente.
Se houver falha em algum teste, você saberá exatamente o código que a causou.
Esse ritmo tranqüilo e constante de desenvolvimento é a maior recompensa do teste do desenvolvedor.
Uma longa sessão de depuração deverá ser exceção.
Como uma mudança feita em uma classe pode afetar algo em outra classe, você deverá esperar reexecutar não só os testes da classe alterada, mas muitos testes.
O ideal é você reexecutar todo o conjunto de testes do seu componente muitas vezes por hora.
Sempre que efetuar uma mudança significativa, reexecute o conjunto, observe os resultados e passe para a mudança seguinte ou corrija a mudança anterior.
Espere gastar algum tempo para possibilitar o feedback rápido.
Automatizar os Testes 
Em geral, a execução de testes não é prática se eles forem manuais.
Para alguns componentes, os testes automatizados são fáceis.
Um exemplo pode ser um banco de dados da memória.
Ele se comunica com os clientes através de uma API e não possui nenhuma outra interface com o mundo externo.
Os testes para o banco de dados seriam mais ou menos assim:
/* Verifique se os elementos podem ser incluídos no máximo uma vez. */
// Configuração
Database db = new Database();
db.add("key1", "value1");
// Teste
boolean result = db.add("key1", "um outro valor");
expect(result == false);
Os testes são diferentes do código de cliente comum em apenas um aspecto: em vez
de acreditarem nos resultados das chamadas de API, eles verificam. Se a API facilitar a criação do código de cliente, facilitará a elaboração do código de teste.
Se o código de teste
não for fácil de ser gravado, você recebeu um aviso antecipado de que
a API poderia ser aprimorada. Dessa forma, o teste anterior ao design é consistente com o enfoque do Rational Unified Process sobre o tratamento antecipado de riscos importantes.
No entanto, quanto maior for a ligação do componente com o mundo externo, maior será a dificuldade em testá-lo.
Há dois casos comuns: interfaces gráficas com o usuário
e componentes de backend.
Interfaces gráficas de usuário
Suponha que o banco de dados do exemplo anterior receba os dados por meio de um callback de um objeto da interface de usuário.
O callback é chamado quando o usuário preenche alguns campos de texto e pressiona um botão.
Testar isso preenchendo os campos manualmente e pressionando o botão muitas vezes por hora não é algo desejável.
Você deve encontrar um modo de entregar a entrada sob o controle programático, geralmente
"pressionando" o botão em código.
A ação de pressionar o botão faz com que alguns códigos do componente sejam executados.
O mais provável é que alterem o estado de alguns objetos da interface do usuário.
Assim, você também deverá encontrar uma forma de consultar esses objetos por meio de programação.
Componentes de back-end
Suponha que o componente submetido ao teste não implemente um banco de dados.
Em vez disso, é um wrapper em torno de um banco de dados real no disco.
Efetuar testes no banco de dados real pode ser difícil.
Instalá-lo e configurá-lo pode ser complicado.
As licenças para usá-lo podem ser caras.
O banco de dados pode reduzir a velocidade dos testes o bastante para você desistir de executá-los com freqüência.
Nesses casos, convém "substituir" o
banco de dados por um componente mais simples que faça apenas o necessário
para suportar os testes.
Os stubs também são úteis quando um componente com o qual o seu componente se comunica ainda não está pronto.
Convém não deixar o teste esperando o código de uma outra pessoa.
Para obter informações adicionais, consulte Conceitos: Stubs.
Não Gravar as Próprias Ferramentas 
O teste do desenvolvedor parece bastante simples.
Você configura alguns objetos, faz uma chamada através de uma API, verifica o resultado e aponta uma falha de teste se os resultados não saírem como esperado.
Convém também encontrar uma forma de agrupar testes para que possam ser executados individualmente ou como conjuntos completos.
As ferramentas que suportam
esses requisitos são chamadas de estruturas de teste.
O teste do desenvolvedor é simples e os requisitos para as estruturas de
teste não são complicadas. No entanto, se cair na tentação de criar sua própria estrutura de teste, você gastará muito mais tempo ajustando-o do que provavelmente imagina.
Existem muitas estruturas de teste disponíveis, tanto comerciais como de código aberto, e não há motivo para não usá-los.
Não Criar Código de Suporte 
O código de teste costuma ser repetitivo.
É comum ver seqüências de códigos como esta:
// nome nulo não permitido
retval = o.createName("");
expect(retval == null);
// espaços iniciais não permitidos
retval = o.createName(" l");
expect(retval == null);
// espaços finais não permitidos
retval = o.createName("name ");
expect(retval == null);
// o primeiro caractere não pode ser numérico
retval = o.createName("5allpha");
expect(retval == null);
Para criar esse código, copie uma verificação, cole-a e edite-a para criar outra verificação.
O perigo aqui é dobrado.
Se a interface for alterada, será necessário um enorme trabalho de edição.
(Em casos mais complicados, uma simples substituição geral não bastará.)
Além disso, se o código for complicado, o objetivo do teste poderá se perder no meio de todo o texto.
Quando você achar que está fazendo muitas repetições, convém seriamente incluí-las em código de suporte.
Embora o código anterior seja apenas um exemplo, será mais fácil lê-lo e mantê-lo se você criá-lo desta forma:
void expectNameRejected(MyClass o, String s) {
Object retval = o.createName(s);
expect(retval == null);
}
...
// nome nulo não permitido
expectNameRejected(o, "");
// espaços iniciais não permitidos.
expectNameRejected(o, " l");
// espaços finais não permitidos.
expectNameRejected(o, "name ");
// o primeiro caractere não pode ser numérico.
expectNameRejected(o, "5alpha");
Em geral, os desenvolvedores que criam testes erram pelo excesso de ações do tipo copiar e colar.
Se você suspeitar que está seguindo essa tendência, convém errar conscientemente na direção contrária.
Decida que você removerá todo o texto duplicado do seu código.
Gravar os Testes Antes 
Elaborar testes depois do código é uma tarefa enfadonha.
Surge uma vontade de executá-los depressa, concluí-los e passar adiante.
Elaborar testes antes do código permite que façam parte de um ciclo de feedback positivo.
À medida que você implementa mais código, mais testes são executados até que, por fim, todos são realizados e concluídos.
As pessoas que gravam testes antes parecem obter mais êxito e não gastam mais tempo nessa tarefa.
Para obter informações adicionais sobre a elaboração de testes
antes, consulte Conceitos: Design de Teste Antes
Manter os Testes Compreensíveis 
Você deve esperar que você mesmo ou outra pessoa tenha que modificar os testes depois.
Uma situação comum é uma iteração posterior exigir uma mudança no comportamento do componente.
Como um simples exemplo, suponha que o componente tenha declarado uma vez um método de raiz quadrada como este:
double sqrt(double x);
Nessa versão, um argumento negativo fazia com que sqrt
retornasse NaN ("não um número" do IEEE 754-1985 Standard for
Binary Floating-Point Arithmetic). Na nova iteração, o método de raiz quadrada aceitará números negativos e retornará um resultado complexo:
Complex sqrt(double x);
Os testes antigos para sqrt terão que ser alterados. Isso
significa entender a função deles e atualizá-los para que funcionem com o
novo sqrt. Ao atualizar testes, tome cuidado para não destruir a capacidade que possuem para detectar erros.
Às vezes, isso acontece desta forma:
void testSQRT () {
// Atualizar estes testes para Complexo
// quando houver tempo -- bem
/*
double result = sqrt(0.0);
...
*/
}
Outras maneiras são mais sutis: os testes são alterados para que realmente sejam executados,
mas não testam mais o que pretendiam originalmente testar. O resultado final, após muitas iterações, pode ser um conjunto de testes precário demais para detectar um grande número de problemas.
Isso às vezes é chamado de "deterioração do conjunto de testes". Um conjunto deteriorado será abandonado porque não compensa mantê-lo.
Não é possível manter a capacidade de localização de erros do teste, a menos que fique claro quais Idéias
de Teste são implementadas por um teste. O código de teste tende a não apresentar comentários,
suficientes, mesmo que seja mais difícil entender a "razão" subjacente do que o código do produto.
A deterioração do conjunto de testes é menos provável nos testes diretos para sqrt
do que nos testes indiretos. Haverá código que chama o sqrt. Esse código terá testes.
Quando o sqrt é alterado,
alguns desses testes falham. A pessoa que altera o sqrt
provavelmente precisará alterar os testes. Como ele está menos familiarizado com os testes e como o relacionamento deles com a mudança é menos claro, é provável que esse indivíduo os enfraqueça no processo de aprovação correspondente.
Quando você estiver criando código de suporte para testes (conforme sugerido acima), tome cuidado: o
código de suporte deve esclarecer, não obscurecer, a finalidade dos testes que o
utilizam. Uma reclamação comum sobre programas orientados a objetos refere-se à inexistência de um local onde uma ação esteja concluída.
Se considerar um método qualquer, você descobrirá apenas que ele encaminha o trabalho correspondente para outro local.
Essa estrutura tem vantagens, mas pessoas com pouca experiência no código têm dificuldade em entendê-lo.
A menos que se esforcem, é provável que as mudanças por elas efetuadas estejam incorretas ou tornem o código ainda mais complicado e frágil.
O mesmo se aplica ao código de teste, exceto pelo fato de ser ainda bem menos provável que os mantenedores tomem o cuidado devido depois.
Para se livrar do problema, elabore testes compreensíveis.
Corresponder a Estrutura do Teste com a Estrutura do Produto 
Suponha que alguém tenha herdado seu componente
e precise alterar uma parte dele.
Essa pessoa pode querer examinar os testes antigos para ajudá-la no novo design.
Ela deseja atualizá-los antes de criar o código (teste anterior ao design).
Todas essas boas intenções de nada adiantarão se não for possível localizar os testes apropriados.
A pessoa efetuará a mudança, verificará os testes com falhas e os corrigirá.
Isso contribuirá para a deterioração do conjunto de testes.
Por esse motivo, é importante que o conjunto de testes seja bem estruturado e o local dos testes seja previsível a partir da estrutura do produto.
Em geral, os desenvolvedores organizam testes em uma hierarquia paralela, com uma classe de teste por classe de produto.
Portanto, se alguém estiver alterando uma classe nomeada Log,
ela saberá que a classe de teste é TestLog e saberá
onde o arquivo de origem pode ser localizado.
Deixar que os Testes Violem o Encapsulamento 
É possível limitar os testes a interagir com o seu componente exatamente como o código de cliente faz, através da mesma interface que esse código usa.
No entanto, há desvantagens.
Suponha que você esteja testando uma classe simples que mantém uma lista duplamente vinculada:

Fig1: Lista Duplamente Vinculada
Especificamente, você está testando o método DoublyLinkedList.insertBefore(Object
existing, Object newObject). Em um dos seus testes, você deseja inserir um elemento no meio da lista e depois verificar se foi inserido com êxito.
O teste usa a lista anterior para criar esta lista atualizada:

Fig2: Lista Duplamente Vinculada - Item Inserido
O teste verifica se a lista está correta desta forma:
// agora a lista é mais longa.
expect(list.size()==3);
// o novo elemento está na posição correta
expect(list.get(1)==m);
// verifique se os outros elementos ainda existem.
expect(list.get(0)==a);
expect(list.get(2)==z);
Isso parece suficiente, mas não é.
Suponha que a implementação da lista esteja incorreta e os ponteiros de regressão não estejam definidos corretamente.
Ou seja, suponha que a aparência da lista atualizada seja realmente esta:

Fig3: Lista Duplamente Vinculada - Falha na Implementação
Se DoublyLinkedList.get(int index) atravessar a
lista do início ao fim (o mais provável), o teste não detectará este defeito.
Se a classe fornecer métodos elementBefore e elementAfter,
a verificação destes defeitos será simples:
// Verificar se todos os links foram atualizados
expect(list.elementAfter(a)==m);
expect(list.elementAfter(m)==z);
expect(list.elementBefore(z)==m); //isso falhará
expect(list.elementBefore(m)==a);
Mas e se esses métodos não forem fornecidos?
Você poderá criar seqüências mais elaboradas de chamadas de método que apresentarão falhas se o suposto defeito estiver presente.
Por exemplo, isto pode funcionar:
// Verificar se o link-reverso de Z está correto.
list.insertBefore(z, x);
// Se incorretamente não foi atualizado, X terá
// sido inserido logo após A.
expect(list.get(1)==m);
No entanto, esse teste requer mais trabalho e provavelmente um esforço de manutenção muito mais difícil.
(A menos que você forneça bons comentários, não estará claro por que o teste está agindo da forma atual.)
Existem duas soluções:
- Incluir os métodos elementBefore e elementAfter
na interface pública. Entretanto, isso expõe efetivamente a implementação a todos e dificulta mudanças futuras.
- Deixar que os testes "observem" e verifiquem os ponteiros diretamente.
Esta última é geralmente a melhor solução, mesmo para uma classe simples como DoublyLinkedList
e especialmente para as classes mais complexas que ocorrem em seus produtos.
Em geral, os testes são inseridos no mesmo pacote da classe que testam.
É concedido acesso protegido ou amigável a eles.
Erros Típicos do Design de Teste
Cada teste avalia um componente e verifica se os resultados estão corretos.
O design
do teste, as entradas que ele utiliza e como ele verifica a exatidão, pode
revelar defeitos com êxito ou pode ocultá-los inadvertidamente. A seguir, são mencionados alguns erros típicos do design de teste.
Defeito ao Especificar Antecipadamente os Resultados Esperados 
Suponha que você esteja testando um componente que converte XML em HTML.
Uma das vontades que surge é obter um exemplo de XML, efetuar a conversão e observar os resultados em um navegador.
Se a aparência da tela está normal, você confirma a versão em HTML salvando-a como o resultado oficial esperado.
Depois disso, um teste compara o resultado real da conversão com o resultado esperado.
Essa prática é perigosa.
Até mesmo usuários avançados de computador estão acostumados a acreditar nas ações do computador.
É provável que você não perceba erros na tela.
(Isso sem falar nos navegadores que são muito tolerantes com problemas de formatação de HTML.)
Ao tornar a versão em HTML incorreta no resultado oficial esperado, você confirma a impossibilidade do teste de encontrar o problema.
É menos arriscado verificar mais uma vez consultando diretamente o HTML, mas essa prática ainda é perigosa.
Como o resultado é complicado, é fácil não perceber os erros.
Você detectará mais defeitos se especificar o resultado esperado manualmente primeiro.
Defeito ao Verificar o Segundo Plano 
Em geral, os testes verificam se uma mudança foi realmente efetuada, mas os respectivos criadores costumam se esquecer de verificar se os itens não incluídos na mudança realmente não foram alterados.
Por exemplo, suponha que um programa tenha que alterar os primeiros 100 registros de um arquivo.
É recomendável verificar se o registro 101 não
foi alterado.
Na teoria, você verificaria se nada no "segundo plano", o
sistema de arquivo inteiro, toda a memória, o tudo que pode ser acessado na rede, foi
esquecido. Na prática, é necessário escolher com cuidado o que é possível verificar.
No entanto, é importante fazer essa escolha.
Defeito ao Verificar a Persistência 
O fato de o componente informar que uma mudança foi efetuada não significa que realmente foi confirmada no banco de dados.
É necessário verificar o banco de dados de outra forma.
Defeito ao Incluir Variedade 
É possível projetar um teste para verificar o efeito de três campos em um registro de banco de dados, mas é necessário preencher muitos outros campos para executá-lo.
Os testadores
geralmente utilizam os mesmos valores repetidamente para esses campos
"irrelevantes". Por exemplo, usam sempre o nome da namorada em um campo de texto ou 999 em um campo numérico.
O problema é que, às vezes, o que parece irrelevante na verdade não é.
Ocasionalmente, há problemas que dependem de uma combinação obscura de informações improváveis.
Se você usar sempre as mesmas informações, não há como detectar esses problemas.
Se variar as informações persistentemente, você poderá detectá-los.
Em geral, não custa quase nada usar um número diferente de 999 ou o nome de uma outra pessoa.
Quando for fácil variar os valores usados nos testes e houver possíveis benefícios nisso, procure variá-los.
(Nota: Não convém utilizar nomes de namoradas antigas em vez da atual
se a namorada atual trabalhar com você.)
Há outro benefício.
Uma falha plausível é o programa utilizar o campo
X quando deveria ter utilizado o campo Y. Se ambos os campos contiverem "Dawn",
a falha não poderá ser detectada.
Defeito ao Utilizar Dados Realistas 
É comum usar dados fictícios em testes.
Em geral, esses dados são simples e irreais.
Por exemplo, os nomes de clientes podem ser "Mickey", "Snoopy" e "Donald".
Como esses dados são diferentes dos inseridos pelos usuários reais - por exemplo, costumam ser mais curtos - podem não detectar defeitos que os clientes reais verão.
Por exemplo, esses nomes de uma palavra não detectariam que o código não trabalha com nomes que contêm espaços.
É aconselhável fazer um pouco mais de esforço para usar dados realistas.
Defeito ao Perceber que o Código Não Faz Nada 
Suponha que você inicialize um registro de banco de dados como zero, efetue um cálculo que deva resultar no armazenamento de zero no registro e verifique que o registro é zero.
O que o teste demonstrou?
Talvez o cálculo não tenha sido efetuado.
Talvez nada tenha sido armazenado e o teste não detectou isso.
Esse exemplo parece improvável.
No entanto, esse mesmo erro pode se manifestar de formas mais sutis.
Por exemplo, você pode elaborar um teste para um programa de instalação complicado.
O objetivo do teste é verificar se todos os arquivos temporários são removidos após uma instalação bem-sucedida.
Porém, por causa de todas as opções de instalação nele presentes, um arquivo temporário específico não foi criado.
É realmente o arquivo que o programa se esqueceu de remover.
Defeito ao Perceber que o Código Atua Incorretamente 
Às vezes, um programa age corretamente por motivos errados.
Como um simples exemplo, considere este código:
if (a < b && c)
return 2 * x;
else
return x * x;
A expressão lógica está errada e você elaborou um teste que permite a ela efetuar avaliações incorretas e seguir a direção errada.
Infelizmente, por pura coincidência, a variável X possui o valor 2 no teste.
Assim, o resultado da direção errada está correto por acaso - o mesmo que seria fornecido pela direção certa.
Para cada resultado esperado, pergunte se há uma forma razoável de o resultado ser obtido pelo motivo errado.
Saber isso é geralmente, mas nem sempre, impossível.
|