Conceito: Teste do Desenvolvedor
Essa diretriz oferece conselhos sobre como superar as primeiras dificuldades de criação de Testes de Desenvolvedor e criação de um conjunto de teste que é mantido por todo o projeto. É também oferecido conselho para criar melhor os Testes de Desenvolvedor.
Relacionamentos
Elementos Relacionados
Descrição Principal

Introdução

A frase "Teste do Desenvolvedor" é utilizada para categorizar as tarefas de teste executadas mais apropriadamente pelos desenvolvedores de software. Ela também inclui os produtos de trabalho criados por essas tarefas. 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, sem ter o trabalho adicional de entregar para grupo de teste separado. O resultado desejado é a descoberta antecipada dos erros de software mais significativos, independentemente de os erros estarem na unidade independente, na integração das unidades ou no trabalho das unidades integradas em um cenário significativo do usuário final.

Introdução de Imprevistos ao Teste de 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 Diretriz: 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 seus 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 Conceito: Stubs.

Não Gravar suas 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 complicados. 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 Primeiro

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 Conceito: 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 esses 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 de Teste à 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.

Permitir aos Testes Violarem 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:

Imagem da Lista Duplamente Vinculada de Amostra

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:

Lista Duplamente Vinculada de Amostra com Imagem Inserida do Item

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:

Lista Duplamente Vinculada de Amostra com Imagem de Falha na Implementação

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:

  1. Inclua 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.
  2. Permita que os testes "procurem a conexão" 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 de Design de Testes de Características

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.

Falha ao Especificar os Resultados Esperados Antecipadamente

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.

Falha 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 foi esquecido no "segundo plano"-o sistema de arquivos inteiro, toda a memória, tudo que pode ser acessado na rede. Na prática, é necessário escolher com cuidado o que é possível verificar. No entanto, é importante fazer essa escolha.

Falha 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.

Falha 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.

Falha ao Utilizar Dados Realísticos

É 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.

Falha ao Observar que o Código Não Executa Absolutamente 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.

Falha ao Observar que o Código Executa o Item Errado

Às vezes, um programa age corretamente por motivos errados. Como um simples exemplo, considere este código:

  if (a < b && c)
    return 2 * x;
diferente
    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.