O design de testes é criado utilizando informações de vários artefatos, incluindo os artefatos de design tais como realizações de caso de uso, modelos de design ou interfaces classificadoras. Os testes são executados após os componentes terem sido criados. É normal criar o design dos testes exatamente no momento deles serem executados - logo após os artefatos de design de software terem sido criados. Veja o exemplo da Figura 1 abaixo. Aqui, o design de testes começa um pouco antes do final da implementação. É construído a partir dos resultados do design dos componentes. A seta que liga a Implementação a Execução dos Testes indica que os testes não podem ser executados até que a implementação esteja completa.
Fig1: Tradicionalmente, o Design de Testes é realizado tardiamente no ciclo de vida
Entretanto, isto não precisa ser dessa forma. Embora a execução dos testes tenha que esperar até que o componente seja implementado, o design dos testes pode ser feito mais cedo. Pode ser feito logo após a conclusão do artefato de design. Pode até mesmo ser feito em paralelo com o design do componente, como mostrado aqui:
Fig2: O Design Teste-primeiro mantém o design de teste cronologicamente alinhado com o design de software
A antecipação do esforço de teste feita desta forma é comumente chamada de "design teste-primeiro". Quais são as suas vantagens?
- Independente de quão cuidadoso você seja no design do software, você cometerá erros. Você pode esquecer um fato relevante. Ou você poderá ter hábitos particulares de pensamento que lhe dificultem ver algumas alternativas. Ou você poderá apenas estar cansado e esquecer alguma coisa. Ter outras pessoas para revisar os artefatos de design ajuda bastante. Eles podem lembrar-se de fatos que você esqueceu, ou ver o que você não conseguiu. Será melhor se essas pessoas tiverem uma perspectiva diferente da sua; olhando o design de forma diferente eles perceberão coisas que você esqueceu.
A experiência tem mostrado que a perspectiva de teste é eficaz. É incansavelmente concreta. Durante o design do software, é fácil pensar em um campo em particular tal como "exibição do cargo do cliente atual" e seguir em frente sem realmente pensar sobre isso. Durante o design dos testes, você deverá decidir especificamente o que esse campo vai exibir quando um cliente, aposentado da Marinha e que obteve legalmente uma patente, insista em ser chamado de "Tenente Morton H. Throckbottle (Apos.), Cel." O seu cargo é "Tenente" ou "Coronel"? Se o design dos testes for adiado até exatamente antes da execução dos testes, como na figura 1, você provavelmente irá desperdiçar dinheiro. Um erro no design do software permanecerá escondido até o design dos testes, quando algum testador disser, "Você sabe, eu conheci este oficial da Marinha...", criar o teste "Morton" e descobrir o problema. Agora uma implementação parcial ou totalmente completa tem que ser re-escrita e um artefato de design terá que ser atualizado. Seria mais barato descobrir o problema antes do início da implementação.
- Alguns erros podem ser capturados antes do design dos testes. Ao invés disso, eles serão capturados pelo implementador. Isto ainda é ruim. A implementação deverá dar uma parada enquanto o foco muda de como implementar o design para o que o design deveria ser. Isso é perturbador mesmo quando os papéis de Implementador e Designer são executados pela mesma pessoa; Quando são por pessoas diferentes, é muito mais. A prevenção desta perturbação é outra forma que o design teste-primeiro ajuda a melhorar a eficiência.
- O design de testes ajuda os implementadores de outra forma, esclarecendo o design. Se houver uma dúvida na cabeça do implementador sobre o significado do design, o design de teste poderá servir como um exemplo específico do comportamento desejado. Isto levará a uma menor quantidade de erros devido a mal-entendidos do implementador.
- Existirão menos erros mesmo se a questão não passou pela cabeça do implementador - mas deveria ter passado. Por exemplo, poderia ter tido uma ambiguidade que o Designer tivesse interpretado inconscientemente de uma forma e o implementador de outra. Se o implementador estiver trabalhando, tanto com o design como com as instruções específicas para o componente fazer o que se deseja - a partir dos casos de teste - o componente estará mais susceptível a fazer o que é realmente necessário.
Aqui estão alguns exemplos para lhe dar o sabor do design teste-primeiro.
Suponha que você está criando um sistema destinado a substituir o antigo método "pedir a secretária" para alocação de salas de reunião. Um dos métodos da classe BdReunião chama-se getReuniao, e tem a seguinte assinatura:
Dado uma pessoa e uma hora, o método getReuniao retorna a reunião que está agendada para a pessoa nessa hora. Se a pessoa não estiver agendada para nada, ele retorna o objeto especial Reunião, não-agendada. Existem alguns casos de teste simples:
- A pessoa não está em nenhuma reunião em uma determinada hora. A reunião não-agendada retornou?
- A pessoa está em uma reunião em uma determinada hora. O método retornou a reunião correta?
Estes casos de teste não são excitantes, mas eles precisam ser testados eventualmente. Eles também podem ser criados agora, escrevendo o código de teste que algum dia irá ser executado. O código Java para o primeiro teste deve ser semelhante a:
Mas existem outras ideias de teste mais interessantes. Por exemplo, este método procura por um uma coincidência. Sempre que um método faz uma pesquisa é uma boa ideia perguntar o que deve acontecer se a pesquisa encontrar mais de uma coincidência. Neste caso, isto significa perguntar "Uma pessoa pode estar em duas reuniões ao mesmo tempo?" Parece impossível, mas perguntar a secretária sobre esse caso pode revelar algo surpreendente. Verifica-se que alguns executivos estão frequentemente agendados em duas reuniões ao mesmo tempo. Seu papel é entrar em uma reunião, fazer a introdução inicial em um curto período de tempo, e depois ir embora. Um sistema que não atenda a este comportamento não será utilizado, pelo menos em parte.
Este é um exemplo do design teste-primeiro feito no nível de implementação que captura um problema de análise. Existem algumas coisas a serem observadas sobre isso:
- Espera-se que uma boa especificação de caso de uso e a análise já tivessem descoberto este requisito. Nesse caso, o problema teria sido evitado "antecipadamente" e getReuniao teria sido projetado de forma diferente. (não poderia retornar uma reunião; deveria retornar um conjunto de reuniões). Porém a análise sempre ignora alguns problemas, e é melhor que eles sejam descobertos durante a implementação do que após a implantação.
- Em muitos casos, os Designers e Implementadores não têm conhecimento suficiente do domínio para capturar tais problemas - eles não terão tempo ou oportunidade de perguntar a secretária. Neste caso, a pessoa responsável pelo design dos testes para o método getReuniao perguntaria, "existe algum caso em que duas reuniões devam ser retornadas?", pensaria por algum tempo e concluiria que não há. O design teste-primeiro não captura todos os problemas, mas o simples fato de fazer o tipo certo de pergunta aumenta a chance de um problema ser encontrado.
- Algumas das mesmas técnicas de testes que se aplicam a implementação aplicam-se igualmente a análise. O design teste-primeiro pode também ser feito por analistas, mas isso não é o tema desta página.
O segundo dos três exemplos é um modelo de gráfico de estados para um sistema de aquecimento.
Fig3: Gráfico de estados HVAC
Um conjunto de testes poderia percorrer todos os arcos no gráfico de estados. Um teste poderia iniciar com um sistema inativo, injetar um evento Muito Quente(Too Hot), fazer o sistema falhar durante o estado de Resfriamento/Execução(Cooling/Running), resolver a falha, injetar outro evento Muito Quente(Too Hot) e então colocar o sistema de volta no estado inativo. Visto que isto não exercita todos os arcos, mais testes são necessários. Esses tipos de testes procuram por vários tipos de problemas de implementação. Por exemplo, ao percorrer cada arco, eles verificam se a implementação deixou algum de fora. Usando seqüências de eventos que têm caminhos de erro seguidos por caminhos que devem concluir com sucesso, eles verificam se o código de tratamento de erro deixou de limpar resultados parciais que possam afetar a computação posterior. (para obter mais informações sobre testes de gráficos de estados, veja Diretriz: Ideias de Teste para Gráficos de Estados e Diagramas de Atividade).
O último exemplo usa parte de um modelo de design. Existe uma associação entre um credor e uma fatura, onde um determinado credor pode ter mais de uma fatura pendente.
Fig4: Associação entre as classes Credor e Fatura
Os testes baseados neste modelo poderão exercitar o sistema quando um credor não tiver nenhuma fatura, uma fatura e várias faturas. Um testador também perguntaria se existem situações em que uma fatura necessite ser associada a mais de um credor, ou uma fatura não tenha nenhum credor. (Talvez as pessoas que atualmente executam o processo em papel, o qual será substituído pelo sistema computacional, usem faturas sem credor para manter o controle dos trabalhos pendentes). Se for verdade, este seria outro problema que deveria ter sido capturado na Análise.
O design teste-primeiro pode ser feito pelo autor do design ou por qualquer outra pessoa. É comum que o autor o faça. A vantagem é que reduz a sobrecarga de comunicação. O Designer e o Designer de Testes não têm que explicar as coisas um para o outro. Um Designer de Testes em separado teria que gastar tempo aprendendo o design, ao passo que o Designer original já o conhece. Finalmente, muitas destas perguntas – tal como "o que acontece se o compressor falhar no estado X?" - são naturais de serem feitas durante o design do artefato de software e o design de teste, de modo que você possa também ter a mesma pessoa perguntando somente uma vez e escrevendo as respostas, sob a forma de testes.
Porém, existem desvantagens. A primeira é que os Designers são, de certa forma, cegos para os seus próprios erros. O processo de design de testes irá revelar algumas dessas cegueiras, mas talvez não tanto quanto uma pessoa diferente iria encontrar. O quão problemático isto é, parece variar muito de uma pessoa para outra e está muitas vezes relacionado com a experiência do designer.
Outra desvantagem de ter a mesma pessoa fazendo tanto o design de software como o design de teste é que não há um paralelismo. Considerando que a atribuição de papéis para pessoas diferentes aumentará o esforço total, isso irá provavelmente resultar em menos tempo útil decorrido. Se as pessoas estiverem desesperadas para sair do design e entrar na implementação, investir tempo no design de testes pode ser frustrante. Mais importante, existe uma tendência de dizer que o trabalho já está concluído para poder entrar na outra fase.
Não. A razão é que nem todas as decisões foram tomadas no design. As decisões feitas durante a implementação não estarão bem-testadas pelos testes criados no design. O exemplo clássico desta situação é uma rotina para ordenar vetores. Existem muitos algoritmos de ordenação com diferentes formas de troca. Para grandes vetores, o Quicksort é normalmente mais rápido do que uma ordenação por inserção, mas frequentemente mais lento para pequenos vetores. Sendo assim um algoritmo de ordenação poderá ser implementado usando o Quicksort para vetores com mais de 15 elementos, mas em caso contrário deve ser usada a ordenação por inserção. Esta divisão de trabalho pode ser invisível para os artefatos de design. Você poderia representá-la em um artefato de design, mas o Designer poderia ter decidido que tornar essas decisões explícitas não seria vantajoso. Uma vez que o tamanho do vetor não interfira no design, o design de teste poderia inadvertidamente usar apenas pequenos vetores, significando que o código do Quicksort não seria testado totalmente.
Como outro exemplo, considere esta parte de um diagrama de sequência. Ela mostra um SecurityManager chamando o método log() de StableStore. Neste caso, porém, o método log() retorna um erro, fazendo com que SecurityManager chame Connection.close().
Fig5: instância do diagrama de sequência de SecurityManager
Este é um bom lembrete para o Implementador. Sempre que log() falhar, a conexão deve ser fechada. A questão é se o Implementador realmente fez isso, e se fez corretamente em todos os casos ou apenas em alguns. Para responder à pergunta, o Designer de Testes deve encontrar todas as chamadas para StableStore.log() e certificar-se que cada um desses pontos de chamada está fornecendo um erro para ser tratado.
Pode parecer estranho executar este tipo de teste, tendo em vista que você apenas verificou os trechos de códigos que chamam StableStore.log(). Você não poderia simplesmente verificar se ele trata os erros corretamente?
Talvez a inspeção pudesse ser o suficiente. Mas o código para tratamento de erros é notoriamente propenso a erros, porque muitas vezes depende implicitamente de pressupostos que a existência do erro tenha violado. O exemplo clássico é o código que trata falhas de alocação. Aqui está um exemplo:
Este código tenta recuperar de um erro de falta de memória, pela limpeza (tornando assim a memória disponível) e então continua a processar eventos. Vamos supor que este é um design aceitável. emergencyRestart toma muito cuidado para não alocar memória. O problema é que emergencyRestart chama alguma rotina utilitária, que chama outra rotina utilitária, que chama outra rotina utilitária e que aloca um novo objeto. Pelo fato de não existir mais memória, então todo o programa falha. Estes tipos de problemas são difíceis de encontrar através de inspeção.
Até este ponto, nós assumimos implicitamente que você poderia fazer a maior quantidade possível de design de testes, o mais rápido possível. Ou seja, você iria derivar todos os testes possíveis a partir dos artefatos de design, acrescentando mais tarde, apenas os testes baseados na implementação. Isto pode não ser adequado na fase de Elaboração, porque tais testes completos podem não estar alinhados com os objetivos de uma iteração.
Suponha que um protótipo arquitetural esteja sendo construído para demonstrar a viabilidade do produto aos investidores. Ele poderá se basear em algumas instâncias dos principais casos de uso. O código deve ser testado para ver se os suporta. Mas existe algum perigo se outros testes forem criados? Por exemplo, poderia ser evidente que o protótipo ignora importantes casos de erro. Porque não documentar a necessidade de tratamento desses erros escrevendo casos de teste que irão exercitá-los?
Mas o que aconteceria se o protótipo fizesse seu trabalho e revelasse que a abordagem arquitetural não funciona? Então, a arquitetura seria jogada fora - juntamente com todos os testes de tratamento de erro. Nesse caso, o esforço de projetar os testes não irá gerar nenhum valor. Teria sido melhor esperar, e apenas projetar os testes necessários para verificar se esse protótipo prova-de-conceito realmente prova o conceito.
Isto pode parecer uma pequena questão, mas existem fortes efeitos psicológicos em jogo. É na fase de Elaboração que são tratados os maiores riscos. Toda a equipe do projeto deve focar estes riscos. Ter pessoas se concentrando em questões menores, desvia o foco e drena a energia da equipe.
Então, onde o design teste-primeiro pode ser usado com sucesso na fase de Elaboração? Ele pode desempenhar um papel importante na exploração adequada dos riscos arquiteturais. A consideração de como a equipe irá saber, precisamente, se um risco tem sido percebido ou evitado, irá adicionar clareza ao processo de design e poderá muito bem resultar em uma melhor construção inicial da arquitetura.
Durante a fase de Construção, os artefatos de design são colocados em sua forma final. Todas as realizações de caso de uso necessárias são implementadas, bem como as interfaces para todas as classes. Pelo fato do objetivo da fase ser exaustivo, é apropriado completar o design teste-primeiro. Eventos posteriores poderão invalidar poucos, ou nenhum teste.
As fases de Concepção e Transição normalmente têm menor foco nas atividades de design para as quais os testes são adequados. Quando forem, o design teste-primeiro é aplicável. Por exemplo, poderia ser usado com o trabalho candidato de prova-de-conceito na Concepção. Assim como nos testes das fases de Construção e Elaboração, ele deve ser alinhado com os objetivos da iteração. |