Conceito: Teste Anterior ao Design
Esta diretriz discute o Teste Anterior ao Design. O Teste Anterior ao Design é aplicado criando primeiro um script de teste, antes de gravar e testar o código. Você continua essa técnica até que não tenha mais nada a testar.
Relacionamentos
Elementos Relacionados
Descrição Principal

Introdução

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.

O Local do Design de Teste é o Diagrama de ciclo de vida

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:

Diagrama do Teste Anterior ao Design

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?

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

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

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

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

Exemplos

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:

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

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

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

Gráfico de Estado HVAC

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.

Diagrama de Associação entre as Classes de Credor e de Fatura

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.

Que Realiza o Teste Anterior ao Design?

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.

Todo o Design de Teste Pode Ser Feito no Tempo de Design do Componente?

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().

Instância do Diagrama de Seqüência do Gerenciador de Segurança

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.

Teste Anterior ao Design e as Fases do RUP

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.