Introdução
O termo "Teste de Desenvolvedor" é usado para classificar as atividades de testes mais apropriadamente realizadas por
desenvolvedores de software. Isto também Inclui os artefatos criados por essas atividades. Os Testes de Desenvolvedor
englobam o trabalho tradicionalmente elaborado nas seguintes categorias: Teste de Unidade, grande parte dos Testes de
Integração e alguns aspectos do que é normalmente chamado de Testes de Sistema. Apesar dos Testes de Desenvolvedor
estarem tradicionalmente associados com as atividades da disciplina de Implementação, eles também têm uma relação com
as atividades da disciplina de Análise e Design.
Pensando nos Testes de Desenvolvedor desta forma "holística", você ajudará a atenuar alguns dos riscos associados com
as abordagens mais "atomizadas", tradicionalmente usadas. Na abordagem tradicional para Testes de Desenvolvedor, o
esforço é inicialmente focado na avaliação de que todas as unidades estejam trabalhando de forma independente.
Posteriormente no ciclo de vida de desenvolvimento, à medida que o trabalho de desenvolvimento se aproxima da
conclusão, as unidades integradas são agrupadas em um sistema ou subsistema executável e testadas desta forma, pela
primeira vez.
Esta abordagem tem uma série de falhas. Em primeiro lugar, porque encoraja uma abordagem em fases para os testes das
unidades integradas e posteriormente os subsistemas, todos os erros identificados durante estes testes são normalmente
encontrados tarde demais. Esta descoberta tardia normalmente resulta na decisão de não tomar nenhuma ação corretiva, ou
na exigência de grande retrabalho para correção. Este retrabalho tanto é caro como susceptível de atrasar o progresso
em outras áreas. Isso aumenta o risco do projeto ser desviado ou abandonado.
Em segundo lugar, quando criamos limites rígidos entre os Testes Unitários, de Integração e de Sistema, aumentamos a
probabilidade de que os erros que estejam no perímetro destes limites não sejam descobertos por ninguém. O risco é
agravado quando a responsabilidade por esses tipos de testes é atribuída a equipes diferentes.
O estilo dos testes de desenvolvedor recomendado pelos processos iterativos, encoraja o desenvolvedor a se concentrar
nos testes mais valiosos e adequados a serem conduzidos em determinado momento. Mesmo no escopo de uma única iteração,
geralmente é mais eficiente para o desenvolvedor localizar e corrigir a maioria dos erros no seu próprio código,
evitando a sobrecarga adicional de passar para outro grupo de testes. O resultado desejado é a rápida descoberta dos
erros de software mais importantes - não importando se estes erros estejam na unidade independente, na integração das
unidades ou no funcionamento das unidades integradas em um cenário de usuário final.
Armadilhas no Início do Uso 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, descobrem que criaram uma suíte de teste impossível de manter e que normalmente é abandonada.
Esta página fornece algumas diretrizes para vencer as primeiras barreiras e para a criação de uma suíte de testes que
evite os problemas de manutenção. Para mais informações, consulte Diretrizes: Mantendo Suítes de Testes Automatizados.
Estabeleça expectativas
Aqueles que acham os testes de desenvolvedor gratificantes irão fazê-los. Aqueles que os veem somente como faina
encontram formas de evitá-los. Trata-se simplesmente na natureza de muitos desenvolvedores na maioria das indústrias, e
tratar isso como uma falta vergonhosa de disciplina 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 re-executa 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.
O fato de ser comum que uma mudança feita em uma classe acarrete erro em outra, implica que você terá que re-executar
não apenas os testes da classe alterada, mas muitos outros. Idealmente, você re-executará a suíte de teste completa
para seu componente muitas vezes por hora. Toda vez que fizer uma alteração significativa, você re-executará a suíte,
observará os resultados e avançará para a próxima alteração ou corrigirá a última alteração. Poderá ser necessário
investir algum esforço para tornar possível esse rápido feedback.
Automatize seus testes
Se os testes forem manuais, executá-los várias vezes pode não ser muito prático. Para alguns componentes, os 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ê já terá um alerta de que a API pode ser
melhorada. O Design Teste-Primeiro é, portanto, coerente com o foco do processo iterativo no cedo tratamento dos riscos
importantes.
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 retorno de chamada ao objeto de interface
de usuário. O retorno de chamada é invocado quando o usuário preenche alguns campos de 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 maneira 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 frequê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 outro que ainda não esteja pronto.
Você não quer que o seu teste fique aguardando pelo código de outra pessoa.
Para mais informações, veja Conceitos: Stubs.
Não escreva suas próprias ferramentas
Os testes de desenvolvedor parecem ser bem simples. Você cria alguns objetos, faz uma chamada através de uma API,
verifica os resultados 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 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 sequências de código como esta:
Este código é criado copiando uma verificação, colando-a e então a editando para fazer outra verificação.
O perigo aqui é duplo. Se a interface mudar, muita edição terá que 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ê suspeitar que esteja nesta
tendência, será mais ú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, veja
Concept: Design Teste-Primeiro.
Mantenha os testes compreensíveis
É possível que você, ou alguém, tenha 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 Padrão
para Aritmética Binária de Ponto Flutuante IEEE 754-1985). 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 teriam que testar. O resultado final, após várias 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.
Você não pode manter um esforço de busca de erros de teste se não estiver claro quais Ideias de Teste um teste implementa. O código de teste tende a ser pouco comentado,
mesmo que seja difícil de entender o "porque" que está por trás do código do produto.
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 alterou sqrt
provavelmente terá que alterar 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 a 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 único 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 eles 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 da suíte de 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. Entretanto, isso tem desvantagens. Suponha que você está testando uma classe
simples que mantém uma lista duplamente encadeada:
Fig1: Lista duplamente-encadeada
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:
Fig2: Lista duplamente-encadeada com item inserido
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 que os ponteiros de
retorno não estejam definidos corretamente. Isto é, suponha que a lista atualizada se parece com:
Fig3: Lista duplamente-encadeada com falha na implementação
Se DoublyLinkedList.get(int index) percorrer a lista do início ao fim (provavelmente), o teste
não perceberá 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 fornecer esses métodos? Você pode conceber sequê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 à interface pública.
Mas que exponha efetivamente a implementação para todos e torne as mudanças futuras mais difíceis.
-
Permita aos 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 se os resultados estão 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.
Falha na especificação dos 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 experientes usuários de computador são levados a acreditar no que o computador faz.
É provável que você ignore erros na apresentação 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.
Falha na verificação da retaguarda
Os testes normalmente verificam se aquilo que deveria ter sido mudado foi mudado, mas seus criadores frequentemente se
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 ideia verificar se o 101o 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 intacto. Na prática, você deve escolher cuidadosamente o que pode verificar. Mas é
importante fazer essa escolha.
Falha na verificação da 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 outro caminho.
Falha na adição de 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 várias 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 poderá ser detectada.
Falha pelo não uso de dados realistas
É comum a utilização de dados criados para os testes. Normalmente esses dados são simples e irreais. 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 em 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 se esqueceu de remover.
Falha em notar que o código faz a coisa errada
Às vezes um programa faz a coisa certa por razões erradas. Como um exemplo trivial, considere o seguinte código:
A expressão lógica está errada, e você escreveu um teste que o faz 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 é.
|