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