Conceitos: Teste Anterior ao Design
Tópicos
Os designs de teste são criados usando informações provenientes de diversos artefatos, inclusive artefatos 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 artefatos 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.

Fig. 1: Tradicionalmente, o Design de Teste é executado mais tarde 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. Poderia ser feito logo após a conclusão do artefato de design. Poderia até ser feito em paralelo com o design do componente, como mostrado aqui:

Fig. 2: 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" é comumente 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 artefatos 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 é eficaz e inexoravelmente concreta. Durante o design do software, é fácil pensar em um determinado campo como "exibindo o cargo do cliente atual" e ir em frente ser pensar sobre isso. Durante o design de teste, você deve decidir especificamente o que o campo mostrará quando um cliente aposentado da Marinha e graduado em Direito insistir em fazer referência a si mesmo como "Tenente Morton H. Throckbottle (Apos.), Esq." A sua patente é "Tenente" ou "Esquire"? Se o design de teste for adiado até pouco antes da execução do teste, como na figura 1, você provavelmente desperdiçará dinheiro. Qualquer erro no design do software não será detectado até o design do teste, quando algum testador disser: "Sabe, conheci esse camarada 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 artefato 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 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 que o Implementador não tivesse a dúvida mas é recomendável que ele tenha tido. Por exemplo, uma ambigüidade pode ter sido interpretada inconscientemente pelo Designer de uma maneira e de outra pelo Implementador. É provável que componente faça o que realmente deve se o Implementador se basear tanto no design como em instruções específicas sobre o que o componente deve fazer a partir de casos de teste .
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 "pergunte à secretária" para designar salas de reunião. Um dos métodos da classe MeetingDatabase é chamado getMeeting e possui esta assinatura:
Meeting getMeeting(Person, Time);
Considerando uma pessoa e um horário, getMeeting retorna a reunião que está programada para essa pessoa naquele horário. Se não houver nenhuma programação para essa pessoa, ele retornará o objeto Meeting como unscheduled. Há alguns casos de teste simples:
- A pessoa não está participando de nenhuma reunião no horário indicado. A reunião unscheduled foi 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:
// if not in a meeting at given time,
// expect to be unscheduled.
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. Nesse caso, isso significa perguntar "Uma pessoa pode estar em duas reuniões ao mesmo tempo?" Parece impossível, mas perguntar à secretária sobre esse caso poderá trazer algumas surpresas. Acontece que é comum alguns executivos terem duas reuniões programadas para o mesmo horário. Seu papel é aparecer em uma reunião, "reagrupar as tropas" por um curto período e depois se retirar. 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 especificação e análise eficientes de caso de uso. Nesse caso, o problema teria sido evitado "no curso inverso" e o getMeeting teria sido designado de forma diferente. (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 implantaçã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 o getMeeting perguntaria: "Há alguma situação em que duas reuniões devam ser retornadas?" Pense um pouco e conclua que não. Portanto, o teste anterior ao design não detecta todos os problemas, mas o simples fato de fazer as perguntas certas aumenta as chances de localizar um problema.
- 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.

Fig. 3: Diagrama de estados 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 obter mais informações sobre diagramas de estados, consulte Diretrizes: Idéias de Teste para Diagramas de Estados e 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.

Fig. 4: Associação entre Credor e Classes 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. Os artefatos Designer e 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 fim, muitas dessas questões como "O que aconteceria se o compressor falhasse no estado X?" são perguntas naturais que devem ser feitas durante o design do artefato de software e o design de teste. Por isso, você também deve 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 artefato 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 artefatos de design. É possível representá-la em um artefato 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. Ele mostra SecurityManager chamando o método log() de StableStore. Nesse caso, porém, o log() retorna uma falha, que faz com que SecurityManager chame Connection.close().

Fig. 5: Instância do diagrama de seqüência do SecurityManager
Esse é um bom lembrete para o Implementador. Sempre que log() falhar, a conexão deverá ser encerrada. 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 essa pergunta, o Designer de Teste deve encontrar todas as chamadas feitas para StableStore.log() e verificar se todos esses pontos de chamada são provenientes de uma falha a ser tratada.
Pode parecer estranho executar esse teste, considerando que você acabou de examinar todo o código que chama 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) { // top level event loop
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 o design seja aceitável. emergencyRestart toma muito cuidado para não alocar memória. O problema é que emergencyRestart chama uma rotina de utilitário, que chama uma segunda rotina, que chama uma terceira rotina, que aloca 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 artefato de design, adicionando 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 investidores. 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 papel 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 artefatos 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.
Copyright
(c) 1987 - 2001 Rational Software Corporation
|