Os designs de teste são criados usando informações provenientes de diversos produtos de trabalho, inclusive produtos de
trabalho de design, como realizações de casos de uso, modelos de design ou interfaces de classificador. Os testes são
executados após a criação dos componentes. É comum criar os designs de teste pouco antes da execução dos testes e após
a criação dos produtos de trabalho de design de software. A figura 1 a seguir mostra um exemplo. Aqui, o design de
teste começa quase no final da implementação. Ele gera os resultados do design do componente. A seta que vai da
Implementação até a Execução do Teste indica que os testes não poderão ser executados enquanto a implementação não for
concluída.
Fig1: Tradicionalmente, o Design de Teste é executado posteriormente no ciclo de vida
Mas não precisa ser sempre assim. Embora a execução do teste tenha de esperar até que o componente seja implementado, o
design de teste pode ser feito antecipadamente. Isso poderia ser feito logo após a conclusão do produto de trabalho de
design. Poderia até ser feito em paralelo com o design do componente, como mostrado aqui:
Fig2: O Teste Anterior ao Design torna o design de teste cronologicamente alinhado ao design de software
Mover o esforço de teste "no curso inverso" é normalmente chamado de "teste anterior ao design". Quais são suas
vantagens?
-
Mesmo que realize o design do software de forma cuidadosa, você cometerá erros. Você poderá esquecer de um fato
relevante ou ter determinados hábitos de pensamento que dificultam ver certas alternativas. Ou ainda, você poderá
simplesmente estar cansado e deixar passar algo. É aconselhável que outras pessoas revisem seus produtos de
trabalho de design. Eles poderão conhecer os fatos que você omitiu ou ver o que você deixou passar. O ideal é que
essas pessoas tenham uma perspectiva diferente; observando o design de outra maneira, elas verão o que você deixou
de ver.
A experiência tem mostrado que a perspectiva do teste é eficiente e inexoravelmente concreta. Durante o design do
software, é fácil pensar em um campo específico como a "exibição do cargo do cliente atual" e continuar sem
realmente pensar nisso. Durante o design de teste, você deve decidir especificamente o que esse campo
mostrará quando um cliente aposentado da Marinha e graduado em direito insistir em fazer referência a si mesmo como
"Tenente Sr. Morton H. Throckbottle (Apos.)" O título dele é "Tenente" ou "Sr."?
Se o design de teste for adiado até pouco antes da execução do teste, como na figura 1, você provavelmente
desperdiçará dinheiro. Um erro no design do software não será detectado até o design de teste, quando algum
testador disser: "Sabe, conheci esse cara da Marinha..." , criar o teste para "Morton" e descobrir o problema. Será
necessário então reescrever uma implementação parcial ou totalmente concluída e atualizar o produto de trabalho de
design. Seria mais econômico detectar o problema antes do início da implementação.
-
Alguns erros podem ser detectados antes do design de teste. Nesse caso , eles serão detectados pelo Implementador.
Isso ainda é ruim. A implementação precisa ser interrompida para que o foco mude de como implementar o design para
como aquele design deve ser. Há uma ruptura mesmo quando os papéis Implementador e Designer são desempenhados pela
mesma pessoa. O grau dessa ruptura é ainda maior quando são pessoas diferentes. Evitar essa ruptura é outra maneira
de o teste anterior ao design ajudar a melhorar a eficiência.
-
Os designs de teste ajudam os Implementadores de uma outra maneira - esclarecendo o design. Se o Implementador
tiver dúvida sobre o significado do design, o design de teste poderá servir como um exemplo específico do
comportamento desejado. O resultado será um menor número de erros decorrentes de equívocos do Implementador.
-
Haverá menos erros mesmo se a pergunta não estivesse na mente do Implementador, mas deveria estar. Por
exemplo, uma ambigüidade pode ter sido interpretada inconscientemente pelo Designer de uma maneira e de outra pelo
Implementador. Se o Implementador estiver trabalhando a partir do design e de instruções específicas sobre o que o
componente deve fazer, a partir dos casos de teste, o componente provavelmente fará o que deve ser feito.
Estes exemplos lhe darão uma idéia sobre o teste anterior ao design.
Suponha que você esteja criando um sistema para substituir o antigo método de "pergunte à secretária" para designar
salas de reunião. Um dos métodos da classe MeetingDatabase é chamado de getMeeting, que tem esta
assinatura:
Meeting getMeeting(Person, Time);
Considerando uma pessoa e um horário, o método getMeeting retorna a reunião em que a pessoa deve estar naquele
momento. Se não houver algo planejado para a pessoa, o objeto especial Reunião não planejada será
retornado. Há alguns casos de teste simples:
-
A pessoa não está participando de nenhuma reunião no horário indicado. A reunião não planejada é retornada?
-
A pessoa está participando de uma reunião nesse horário. O método retornou a reunião correta?
Esses casos de teste não são interessantes, mas acabam precisando ser experimentados. Eles também podem ser criados
nesse ponto, escrevendo o código de teste real que um dia será executado. O código Java para o primeiro teste pode ter
esta aparência:
// se não estiver em uma reunião no horário indicado, // espera-se que não esteja planejada. public void testWhenAvailable() { Person fred = new Person("fred"); Time now = Time.now(); MeetingDatabase db = new MeetingDatabase(); expect(db.getMeeting(fred, now) == Meeting.unscheduled); }
Contudo, existem idéias de teste mais interessantes. Por exemplo, esse método procura uma correspondência. Sempre que
um método faz uma pesquisa, é aconselhável perguntar o que deverá acontecer se a pesquisa encontrar mais de uma
correspondência. Neste caso, isso significa perguntar "Uma pessoa pode estar em duas reuniões ao mesmo tempo?" Parece
impossível, mas perguntar isso à secretária pode revelar algo surpreendente. Acontece que é comum alguns executivos
terem duas reuniões programadas para o mesmo horário. Sua função é aparecer em uma reunião, "reunir-se" por um curto
período de tempo e, em seguida, continuar. Um sistema que não tenha acomodado esse comportamento pode ficar
parcialmente inutilizado.
Este é um exemplo do teste anterior ao design realizado no nível de implementação, que detectou um problema de análise.
Vários fatores devem ser observados:
-
Você gostaria que esse requisito já tivesse sido descoberto por definição e análise eficientes no caso de uso.
Nesse caso, o problema teria sido evitado "no curso inverso" e getMeeting teria sido projetado de outra
maneira. (Ele não poderia retornar uma reunião; teria de retornar um conjunto de reuniões.) Como a análise sempre
deixa passar alguns problemas, é melhor que eles sejam descobertos durante a implementação, e não após a
implementação.
-
Em muitos casos, os Designers e Implementadores não terão conhecimento do domínio para detectar esses problemas
eles não terão oportunidade nem tempo de perguntar à secretária. Nesse caso, a pessoa que projeta testes para
getMeeting perguntaria, "Há alguma situação em que duas reuniões devem ser retornadas?", pense um pouco e
conclua que não. Então o teste anterior ao design não captura todos os problemas, mas o mero fato de perguntar os
tipos certos de perguntas aumenta a chance de que um problema seja encontrado.
-
Algumas das mesmas técnicas de teste aplicáveis durante a implementação também se aplicam na análise. O teste
anterior ao design também pode ser realizado por analistas, mas não abordaremos isso nesta página.
O segundo dos três exemplos é um modelo de diagrama de estados para um sistema de calefação.
Fig3: Diagrama de Estado HVAC
Um conjunto de testes percorre todos os arcos no diagrama de estados. O teste pode iniciar com um sistema ocioso,
inserir um evento Quente Demais, gerar uma falha no sistema durante o estado de Refrigeração/Funcionamento, eliminar a
falha, inserir outro evento Quente Demais e, em seguida, retornar o sistema ao estado Ocioso. Como esse procedimento
não testa todos os arcos, mais testes serão necessários. Esses tipos de teste procuram vários problemas de
implementação. Por exemplo, percorrendo todos os arcos, eles verificam se a implementação deixou algum de fora. Através
de seqüências de eventos que tenham caminhos de falha seguidos de caminhos que devam ser concluídos com êxito, eles
verificam se o código de tratamento de erros deixou de eliminar os resultados parciais que possam afetar a computação
posterior. (Para saber mais sobre os gráficos de estado, consulte Diretriz de Produto de Trabalho: Idéias de Teste para o Gráfico de Estado e os Diagramas
de Atividades.)
O último exemplo usa parte de um modelo de design. Há uma associação entre um credor e uma fatura, em que qualquer
credor pode ter mais de uma fatura pendente.
Fig4: Associação entre as Classes de Credor e de Fatura
Os testes baseados nesse modelo examinam o sistema quando um credor não tem nenhuma fatura ou quando tem uma ou várias
faturas. O testador também perguntaria se há situações em que uma fatura precise ser associada a mais de um credor ou
situações em que não haja credor. (Pessoas que geralmente usam o sistema em papel, o qual será substituído pelo sistema
computacional, usam as faturas como meio de manter o controle do trabalho pendente). Se for o caso, esse seria mais um
problema que deveria ter sido identificado na Análise.
O teste anterior ao design pode ser realizado pelo autor do design ou por outra pessoa, mas é mais comum o autor
realizá-lo. A vantagem é que isso reduz a sobrecarga de comunicação. O Designer do produto de trabalho e o Designer de
Teste não precisam explicar nada um para o outro. Além disso, um outro Designer de Teste também levaria tempo para
aprender o design, ao passo que o Designer original já o conhece. Por último, muitas das perguntas, como "o que
acontece se o compressor falhar no estado X?", são perguntas naturais a serem feitas durante o design de de produto de
trabalho e design de teste do software, então você pode pedir que a mesma pessoa faça essas perguntas uma vez e escreva
as respostas na forma de testes.
Há algumas desvantagens, porém. A primeira é que o produto de trabalho Designer, até certo ponto, não consegue enxergar
os seus próprios erros. O processo de design de teste revelará parte dessa incapacidade, mas provavelmente não tanto
quanto outra pessoa o faria. A dimensão desse problema parece variar muito de pessoa para pessoa e geralmente está
relacionada à experiência do Designer.
Outra desvantagem de a mesma pessoa realizar tanto o design de software como o design de teste é que não há
paralelismo. Considerando que a alocação dos papéis a pessoas distintas gera mais esforço total, o resultado
provavelmente será menor tempo decorrido. Para pessoas que estão ansiosas para passar do design para a implementação,
perder tempo com design de teste pode ser frustrante. O mais importante é que existe uma tendência a realizar o
trabalho de maneira deficiente para continuar o processo.
Não. O motivo é que nem todas as decisões são tomadas na fase de design. As decisões tomadas durante a implementação
não serão bem experimentadas por testes criados a partir do design. O exemplo clássico é uma rotina para classificar
matrizes. Há vários algoritmos de classificação com diversas vantagens e desvantagens. Em geral, o Quicksort é mais
rápido que uma classificação por inserção em matrizes extensas, mas costuma ser mais lento em matrizes menores.
Portanto, um algoritmo de classificação pode ser implementado para usar o Quicksort em matrizes com mais de 15
elementos, diferentemente da classificação por inserção. Essa divisão de trabalho pode não ser visível a partir de
produtos de trabalho de design. É possível representá-la em um produto de trabalho de design, mas o Designer
pode ter decidido que não vale a pena tomar essas decisões explícitas. Como o tamanho da matriz não tem importância no
design, o design de teste pode usar inadvertidamente apenas matrizes pequenas, o que significa que nenhuma parte do
código Quicksort seria testada.
Como outro exemplo, considere essa fração de um diagrama de seqüência. Ela mostra um SecurityManager chamando o
método log() de StableStore. Neste caso, o log() retorna um defeito, que faz
SecurityManager chamar Connection.close().
Fig5: Instância do diagrama de seqüência do GerenciadordeSegurança
Esse é um bom lembrete para o Implementador. Sempre que log() falhar, a conexão deverá ser fechada. A pergunta a
ser respondida pelo teste é se o Implementador realmente o realizou, e se o realizou corretamente, em todos os
casos ou apenas em alguns. Para responder a pergunta, o Designer de Teste deve localizar todas as chamadas para
StableStore.log() e certificar-se de que cada um desses pontos de chamada recebeu um defeito a ser manipulado.
Pode parecer estranho executar esse teste, considerando que você acabou de verificar todos os códigos que chamam
StableStore.log(). Será que não é possível simplesmente verificar se ele trata a falha corretamente?
Talvez a inspeção seja o bastante. Mas o código de tratamento de erros é notoriamente propenso a erros, pois geralmente
baseia-se em suposições que foram violadas pela existência do erro. O exemplo clássico é o código que trata falhas de
alocação. Eis um exemplo:
while (true) { // loop de eventos no primeiro nível try { XEvent xe = getEvent(); ... // main body of program } catch (OutOfMemoryError e) { emergencyRestart(); } }
Esse código tenta se recuperar de erros de falta de memória fazendo a limpeza (disponibilizando assim a memória) e
continuando a processar os eventos. Vamos supor que seja um design aceitável. emergencyRestart toma muito
cuidado para não alocar memória. O problema é que a emergencyRestart chama algumas rotinas de utilitário, que
chamam outras rotinas de utilitário, que chamam outras rotinas de utilitário - que alocam um novo objeto. Se não houver
memória, o programa inteiro falhará. Esses tipos de problemas são difíceis de localizar através de inspeção.
Supomos até aqui que o máximo de design de teste seria realizado com a maior antecedência possível. Ou seja, você
geraria todos os testes possíveis a partir do produto de trabalho de design, incluindo posteriormente apenas os testes
baseados em itens internos da implementação. Isso pode não ser apropriado na fase de Elaboração, pois esse teste
completo pode não estar de acordo com os objetivos de uma iteração.
Suponha que um protótipo de arquitetura esteja sendo criado para demonstrar a viabilidade do produto aos envolvidos.
Ele pode ser baseado em algumas instâncias principais de caso de uso. O código deve ser testado para verificar se ele
as suporta. Mas haverá algum problema se testes adicionais forem criados? Por exemplo, pode ser óbvio que o protótipo
ignora casos de erro importantes. Por que não documentar a necessidade desse tratamento de erros elaborando casos de
teste que o experimentarão?
E se o protótipo cumprir sua função e revelar que a abordagem arquitetural não funcionará? Então, a arquitetura será
descartada - juntamente com todos os testes referentes ao tratamento de erros. Nesse caso, o esforço de projetar os
testes terá sido em vão. O ideal teria sido esperar e projetar apenas os testes necessários para verificar se esse
protótipo de prova de conceito realmente prova o conceito.
Esse parece ser um ponto secundário, mas há fortes efeitos psicológicos em jogo. A fase de Elaboração lida com os
principais riscos. Toda a equipe do projeto deve se concentrar nesses riscos. Se a atenção das pessoas estiver voltada
para questões secundárias, o foco e a energia da equipe serão consumidos.
Então, em que parte da fase de Elaboração o teste anterior ao design pode ser usado com êxito? Ele pode desempenhar um
função importante na exploração dos riscos arquiteturais. Se a equipe conseguir saber com precisão se um risco foi
percebido ou evitado, o processo de design será mais claro e o resultado provavelmente será uma arquitetura será mais
bem criada desde o início.
Durante a fase de Construção, os produtos de trabalho de design assumem sua forma final. Todas as realizações de casos
de uso necessárias são implementadas, bem como as interfaces de todas as classes. Como o objetivo da fase é a
abrangência, é aconselhável concluir o teste anterior ao design. Eventos posteriores devem invalidar alguns testes (ou
nenhum).
As fases de Iniciação e de Transição normalmente abrangem atividades menos centradas no design, para as quais o teste é
apropriado. Nesse caso, o teste anterior ao design é aplicável. Por exemplo, ele poderia ser usado na sugestão de prova
de conceito na Iniciação. Como no caso do teste da fase de Construção e de Elaboração, deve estar de acordo com os
objetivos da iteração.
|