Armadilhas no Início da Utilização de Testes de
Desenvolvedor
Muitos desenvolvedores, que começam a fazer um trabalho substancialmente mais profundo de teste, desistem do esforço
rapidamente. Eles acham que os testes não serão produtivos. Além disso, alguns desenvolvedores que começam bem com os
testes de desenvolvedor acham que eles criaram uma suíte de teste de manutenção impossível, que é normalmente
abandonada.
Estabeleça expectativas
Aqueles que acham os testes de desenvolvedor gratificantes irão fazê-los. Aqueles que os vêem somente como fardo
encontram formas de evitá-los. Trata-se simplesmente na natureza de muitos desenvolvedores na maioria das indústrias, e
tratá-lo como uma falta de disciplina vergonhosa não tem sido historicamente bem sucedido. Portanto, como um
desenvolvedor você deve esperar que os testes sejam gratificantes e fazer o que for preciso para torná-los
gratificantes.
Os testes de desenvolvedor ideais seguem um ciclo edição-teste muito curto. Você faz uma pequena alteração no produto,
tal como adicionar um novo método a uma classe, e então reexecuta seus testes imediatamente. Se algum teste não passar,
você saberá exatamente qual código causou a falha. Este ritmo de desenvolvimento, fácil e seguro, é a maior recompensa
dos testes de desenvolvedor. Uma longa sessão de depuração deverá ser excepcional.
Pelo fato de ser comum que uma mudança feita em uma classe acarrete erro em outra, você deve esperar a reexecução não
apenas dos testes da classe alterada, mas muitos outros. Idealmente, você reexecuta a suíte de teste completa para seu
componente muitas vezes por hora. Toda vez que você fizer uma alteração significativa, você reexecutará a suíte,
observará os resultados e avançará para a próxima alteração ou corrigirá a última alteração. Espere investir algum
esforço para tornar possível esse rápido feedback.
Automatize seus testes
Se os testes forem manuais, executa-los várias vezes pode não ser muito prático. Para alguns componentes, testes
automatizados são fáceis. Um exemplo seria uma base de dados em memória. Ela se comunica com seus clientes através de
uma API e não tem nenhuma outra interface com o mundo exterior. Os testes para ela seriam semelhantes a este:
Os testes são diferentes do código cliente normal em um único aspecto: ao invés de confiar nos resultados das chamadas
a API, eles os verificam. Se a API tornar mais fácil a escrita do código cliente, também tornará mais fácil a escrita
do código de teste. Se o código de teste não for fácil de escrever, você receberá um alerta de que a API pode
ser melhorada. O design teste-primeiro é, portanto, coerente com o foco no cedo tratamento dos riscos importantes do
processo iterativo.
Quão mais estreitamente ligado ao mundo exterior o componente for, mais difícil será de testá-lo. Existem dois casos
comuns: interfaces gráficas de usuário e componentes de retaguarda.
Interfaces gráficas de usuário
Suponha que o banco de dados no exemplo acima receba seus dados através de um retorne de chamada a objeto de interface
de usuário. O retorno de chamada é invocado quando o usuário preenche alguns campos texto e pressiona um botão.
Testá-lo manualmente preenchendo os campos e pressionando o botão não é algo que você deseje fazer várias vezes
seguidas. Você tem que arranjar uma forma de efetuar a entrada sob controle programático, normalmente "pressionando" o
botão via código.
Pressionar o botão irá fazer com que algum código no componente seja executado. É provável, que o código mude o estado
de alguns objetos da interface de usuário. Sendo assim você também deve arranjar uma forma de consultar esses objetos
de forma programática.
Componentes de retaguarda
Suponha que o componente em teste não implemente uma base de dados. Ao invés, é uma emulação de uma base de dados em
disco. O teste em uma base de dados verdadeira poderia ser difícil. Talvez seja difícil de instalar e configurar. As
licenças para isso podem ser caras. A base de dados poderá tornar os testes lentos o bastante para que você não fique
inclinado a executá-los com freqüência. Nestes casos, vale a pena substituir a base de dados por um simples componente
que seja suficiente para permitir a execução dos testes.
Essas substituições também são úteis quando o seu componente tem que interagir com um outro que ainda não esteja
pronto. Você não quer que o seu teste fique aguardando pelo código de outra pessoa.
Não escreva suas próprias ferramentas
Os testes de desenvolvedor parecem bastante simples. Você cria alguns objetos, faz uma chamada através de uma API,
verifica o resultado e anuncia a falha do teste se os resultados não forem os esperados. É também conveniente ter
alguma forma de agrupar os testes, a fim de que eles possam ser executados individualmente ou como suítes completas. As
ferramentas que suportam esses requisitos são chamadas de frameworks de teste.
Os testes de desenvolvedor são simples, e os requisitos para os frameworks de teste não são complicados. Se, no
entanto, você cair na tentação de escrever seu próprio framework de teste, você poderá gastar muito mais tempo com
ajustes no framework do que você provavelmente esperava. Existem muitos frameworks de teste disponíveis, tanto
comerciais como de código aberto, e não há nenhuma razão para não usar um deles.
Crie código de suporte
O código do teste tende a ser repetitivo. É comum ver seqüências de código como esta:
Este código é criado copiando uma verificação, colando-a e então a editando para fazer uma outra verificação.
O perigo aqui é duplo. Se a interface mudar, muita edição terá de ser feita. Nos casos mais complexos, uma simples
substituição global, não basta.) Também, se o código for muito complicado, a intenção do teste pode ficar perdida no
texto.
Quando você notar que está se repetindo, considere seriamente a fatoração das repetições em código de suporte. Embora o
código acima seja um mero exemplo, ficará mais legível e manutenível se for escrito assim:
Os desenvolvedores escrevem testes muitas vezes errados pelo fato de copiar-e-colar. Se você suspeita que esteja nesta
tendência, é útil errar conscientemente na outra direção. Limpe seu código de todo texto duplicado.
Escreva os testes primeiro
Escreve os testes após o código é uma faina. A urgência é correr com eles, para terminá-los e seguir em frente.
Escrever os testes antes do código faz parte de um ciclo de feedback positivo. À medida que você implemente mais
código, você verá mais testes passarem até finalmente todos os testes executarem com sucesso. As pessoas que escrevem
os testes primeiro aparentam ser mais bem sucedidas, e isso não requer muito tempo. Para obter mais informações sobre a
colocação dos testes em primeiro lugar, veja Guideline: Design Teste-primeiro.
Mantenha os testes compreensíveis
Você deve esperar que você, ou alguém, terá que modificar os testes posteriormente. Uma situação típica é que uma
iteração posterior exija uma mudança no comportamento de um componente. Por exemplo, suponha que o componente tenha
declarado um método para raiz quadrada como este:
Nessa versão, um argumento negativo faz com que sqrt retorne NaN ("não é um número" do ieee
754-1985 Padrão para Aritmética Binária de Ponto Flutuante). Na nova iteração, o método para raiz quadrada
aceitará números negativos e retornará um resultado complexo:
Os testes anteriores para sqrt terão que ser alterados. O que significa compreender o que eles
fazem, e atualizá-los para que funcionem com o novo método sqrt. Ao atualizar os testes, você
deve tomar cuidado para não destruir o seu poder de encontrar erros. Uma forma que às vezes acontece é a seguinte:
Outras formas são mais sutis: os testes foram alterados para que realmente funcionem, mas eles já não testam o que
originalmente eles teriam que testar. O resultado final, ao longo de muitas iterações, poderá ser uma suíte de teste
muito fraca que não detecte vários erros. Isto é normalmente chamado de "decadência da suíte de teste". Uma suíte
decadente será abandonada, porque sua manutenção não é viável.
A decadência de suítes de teste é menos provável nos testes diretos para sqrt do que nos
indiretos. Haverá um código que evocará sqrt. Este código terá testes. Quando sqrt for alterada, alguns destes testes irão falhar. A pessoa que mantém sqrt
provavelmente terá que manter esses testes. Pelo fato dela estar menos familiarizada com eles, e do seu relacionamento
com a mudança ser menos claro, ela estará mais suscetível de enfraquecê-los no processo de fazê-los passar.
Quando você estiver criando código de suporte para os testes (como descrito acima), tenha cuidado: o código de suporte
deve esclarecer, e não obscurecer, o propósito dos testes que o usam. Uma queixa comum sobre programas orientados a
objeto é que não existe um lugar onde tudo seja feito. Se você olhar para qualquer método, tudo o que você descobre é
que ele transmite o trabalho para outro lugar. Essa estrutura tem vantagens, mas torna mais difícil aos novatos a
compreensão do código. A menos que façam um esforço, suas alterações poderão ser incorretas ou tornar o código ainda
mais complicado e frágil. O mesmo é válido para código de teste, exceto pelo fato de que será ainda menos provável que
os mantenedores tomem o devido cuidado mais tarde. Você deve evitar o problema escrevendo testes compreensíveis.
Iguale a estrutura do teste com a do produto
Suponha que uma pessoa tenha herdado o seu componente. Ela precisa mudar uma parte dele. Ela pode querer examinar os
testes antigos para ajudá-la em seu novo design. Ela quer atualizar os testes antigos antes de escrever o código
(design teste-primeiro).
Todas essas boas intenções serão inúteis, se ela não puder encontrar os testes apropriados. Ela fará a alteração,
verificará as falhas e corrigirá os testes. Isto irá contribuir para a decadência do teste.
Por essa razão, é importante que a suíte de teste seja bem estruturada, e que a localização dos testes seja previsível
na estrutura do produto. Geralmente, os desenvolvedores organizam os testes em uma hierarquia paralela, com uma classe
de teste para cada classe do produto. Portanto, se alguém estiver alterando uma classe chamada Registro, ele saberá que a classe de teste se chamará TestaRegistro, e saberá
onde o arquivo fonte pode ser encontrado.
Deixe os testes violarem o encapsulamento
Você pode limitar seus testes para interagir com o seu componente exatamente como o código cliente faz, através da
mesma interface que o código cliente usa. No entanto, isso tem desvantagens. Suponha que você está testando uma classe
simples que mantém uma lista duplamente ligada:
Em particular, você está testando o método doublylinkedlist.insertbefore(Objeto existente, Objeto
newObject). Em um de seus testes, você pretende inserir um elemento no meio da lista e verificar se ele foi
inserido com sucesso. O teste usa a lista acima para criar esta lista atualizada:
Ele verifica a exatidão da lista assim:
Isto parece ser suficiente, mas não é. Suponha que a implementação da lista esteja incorreta e os ponteiros de retorno
não estejam definidos corretamente. Isto é, suponha que a lista atualizada se parece com:
Se doublylinkedlist.get(int index) percorrer a lista do início ao fim (provavelmente), o teste
não perceberia esta falha. Se a classe fornecer os métodos elementbefore e elementafter, a verificação destas falhas será simples:
Mas e se ela não fornecesse esses métodos? Você pode conceber seqüências de chamadas de método mais elaboradas que irão
falhar se o defeito suspeito estiver presente. Por exemplo, isto poderia funcionar:
Mas este teste é mais trabalhoso de criar e provavelmente será significativamente mais difícil de manter. (A menos que
você escreva bons comentários, não ficará claro porque o teste está fazendo o que faz). Existem duas soluções:
-
Adicione os métodos elementBefore e elementAfter a interface pública.
Mas que exponha efetivamente a implementação para todos e torne as mudanças futuras mais difíceis.
-
Deixe os testes "olharem sob a capa" e marque ponteiros diretamente.
Esta é geralmente a melhor solução, mesmo que seja para uma simples classe como doublylinkedlist
e especialmente para as classes mais complexas que existem em seus produtos.
Normalmente, os testes são colocados no mesmo pacote onde estão as classes que eles verificam. Eles fornecem acesso
amigo ou protegido.
Erros Característicos de Design de Teste
Cada teste exercita um componente e verifica resultados corretos. O design do teste, as entradas que ele usa e como
verifica se o resultado está correto, podem ser bons para revelar defeitos, ou podem inadvertidamente ocultá-los. Aqui
estão algumas características dos erros no design de testes.
Incapacidade para especificar os resultados esperados com antecedência
Suponha que você está testando um componente que converte XML em HTML. É uma tentação pegar algumas amostras de XML,
executar a conversão e, em seguida, analisar os resultados em um navegador. Se a tela parecer correta, você "abençoa" o
HTML e o salva como o resultado esperado oficial. Depois, um teste compara os resultados reais da conversão com os
resultados esperados.
Esta é uma prática perigosa. Mesmo usuários de computador sofisticados são levados a acreditar no que o computador faz.
É provável que você ignore erros na aparência da tela. (Sem mencionar que navegadores são bastante tolerantes a HTML
malformado). Ao tornar esse HTML incorreto o resultado esperado oficial, você garantirá que o teste nunca poderá
encontrar o problema.
É menos perigoso executar uma verificação-dupla, olhando diretamente para o HTML, mas isso ainda é perigoso. Por causa
da saída complicada, será fácil não perceber os erros. Você encontrará mais defeitos se você escrever manualmente a
saída esperada primeiro.
Impossibilidade de verificar a retaguarda
Os testes normalmente verificam se aquilo que deveria ter sido mudado foi mudado, mas seus criadores frequentemente
esquecem de verificar se aquilo que deveria ter sido deixado de lado foi deixado de lado. Por exemplo, suponha que um
programa deva alterar os 100 primeiros registros em um arquivo. É uma boa idéia verificar se o 101º não foi
alterado.
Em teoria, você deveria verificar que tudo, na "retaguarda"; todo o sistema de arquivos, toda a memória, tudo que for
acessível pela rede; foi deixado em paz. Na prática, você deve escolher cuidadosamente o que você pode verificar. Mas é
importante fazer essa escolha.
Impossibilidade de verificar a persistência
Só porque o componente lhe disse que uma mudança foi feita, não significa que tenha sido efetivamente executada na base
de dados. Você precisa verificar a base de dados através de uma outra via.
Incapacidade para adicionar variedade
Um teste pode ser projetado para verificar o efeito de três campos em um registro da base de dados, mas muitos outros
campos devem ser preenchidos para executar o teste. Os Testadores irão normalmente usar os mesmos valores outras vezes
para estes campos "irrelevantes". Por exemplo, eles sempre usarão o nome da namorada em um campo texto, ou 999 em um
campo numérico.
O problema é que, o que não importa, às vezes é realmente importante. Quantas vezes existem problemas que dependem de
alguma combinação obscura de entradas improváveis. Se você usar sempre as mesmas entradas, você não terá nenhuma chance
de encontrar esses erros. Se você variar as entradas persistentemente, você poderá encontrar os erros. Muitas vezes,
não custa nada utilizar um número diferente de 999 ou usar o nome de outra pessoa. Quando a variação dos valores
utilizados em testes não custar nada e algum benefício potencial for gerado, então varie. (Nota: é desaconselhável a
utilização dos nomes das namoradas antigas se a atual estiver trabalhando com você.)
Aqui está mais um benefício. Uma falha plausível é quando o programa usa o campo X quando deveria ter usado o
campo Y. Se ambos os campos contiverem "Alvorada", a falha não pode ser detectada.
Falha no uso de dados realistas
É comum a utilização de dados criados para os testes. Normalmente esses dados são excessivamente simples. Por exemplo,
os nomes de clientes podem ser "mickey", "snoopy" e "donald". Pelo fato destes dados serem diferentes do que os
usuários realmente irão entrar - caracteristicamente curtos - eles poderão esconder defeitos reais que os clientes irão
descobrir. Por exemplo, esses nomes com somente uma palavra não detectarão que o código não pode tratar nomes com
espaços.
É prudente fazer um pequeno esforço extra para usar dados realistas.
Falha para notar que o código simplesmente não faz nada
Suponha que você inicializou um registro na base de dados com zeros, executou um cálculo que resultará em zero e será
armazenado no registro e, em seguida, irá verificar se o registro é zero. O que o seu teste demonstrou? O cálculo pode
simplesmente não ter acontecido. Pode não ter sido armazenado nada, e o teste não irá identificar.
Este exemplo parece pouco provável. Mas esse mesmo erro pode surgir de forma sutil. Por exemplo, você pode escrever um
teste para um complexo programa de instalação. O teste destina-se a verificar que todos os arquivos temporários serão
removidos após uma instalação com sucesso. Mas, por causa das opções do instalador, um determinado arquivo temporário
não foi criado no teste. Certamente, esse é o que o programa esqueceu de remover.
Falha para notar que o código faz a coisa errada
Às vezes um programa faz a coisa certa para motivos errados. Como um exemplo trivial, considere o seguinte código:
A expressão lógica está errada, e você escreveu um teste que o faça avaliar incorretamente e escolher o caminho errado.
Infelizmente, por pura coincidência, a variável X tem o valor 2, no teste. Sendo assim o resultado do caminho errado
está acidentalmente correto - o mesmo resultado aconteceria se o caminho correto fosse seguido.
Para cada resultado esperado, você deve perguntar se há uma forma plausível desse resultado ser alcançado pelo motivo
errado. Mesmo sendo muitas vezes impossível de saber, às vezes não é.
|