Diretriz: Testes de Desenvolvedor
Esta diretriz descreve técnicas para o início da utilização dos testes de desenvolvedor e também as características de bons testes de desenvolvedor.
Relacionamentos
Elementos Relacionados
Descrição Principal

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:

  1. 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.
  2. 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 é.