Após um breve exercício de análise, você faz um levantamento das tarefas que precisará fazer para concluir a criação de uma determinada funcionalidade no software. Desta funcionalidade, tanto você como o usuário para o qual o software se destina, pode estabelecer as condições mínimas necessárias para que o software seja considerado "satisfatório".
Estas condições poderão fazer parte do Teste de Aceitação, um conjunto de critérios definidos com o usuário, em forma de estórias ou uma descrição formal dos resultados esperados, que servirá como guia na hora de criar os testes e desenvolver o software.
A interação com o usuário é um fator fundamental para o estabelecimento das regras, condições e cenários que o software deverá atender. Sua participação na especificação é decisiva para um software ter sucesso ou o projeto cair por terra.
Se ao desenvolver o software você não interage diretamente com seu usuário final, como por exemplo, em caso do desenvolvimento de um game, um driver, um serviço (para um sistema operacional), etc., é recomendado que, ainda assim, você reuna a equipe de desenvovimento junto aos especialistas no domínio e crie um conjunto de espectativas sobre o software que devem ser seguidas, para poder avaliar, posteriormente, se o desenvolvimento do mesmo está caminhando na direção certa.
Agora, se você pode ter contato com o usuário final, o faça! Discuta todos os detalhes possíveis e imaginários, para que a solução desenvolvida avalie diversos cenários do "mundo real" que possam vir a ocorrer e qual a solução para cada uma, além de todas as verificações que se espera do software (o que será muito importante para a criação dos testes).
E documente. Amanhã, se o usuário disser que algo no software não atende às suas necessidades, mas tudo o que foi desenvolvido seguiu a fio a definição que ele deu, você terá em mãos um documento assinado que descreve o que ele mesmo disse que esperava.
O óbvio e o implícito
Não espere demais do usuário. O usuário espera o óbvio. Que o programa funcione sem erros de lógica, que os cálculos estejam corretos, que os dados sejam gravados com sucesso, ..., enfim, que tudo ocorra como esperado - apesar de "o esperado" não ter ficado explícito em nenhum documento formal, ou nem mesmo tenha sido mencionado no levantamento de requisitos.
Cabe então à sua equipe especificar quais coisas devem ser verificadas para garantir o mínimo que qualidade na funcionalidade criada. Um brainstorming ajuda muito nestes casos. Começa-se pelo trivial. Depois, à medida em que se pensa no problema, novas verificações possivelmente necessárias vão sendo descobertas, até que se chegue a uma boa cobertura das prováveis falhas.
Nesse ponto, cada membro da equipe começa a implementar a sua parte da funcionalidade, começando pela codificação dos testes que verificarão o código que por ele será implementado.
Após codificar um teste, se escreve o código para passar no mesmo e, então, o refatora. Segue-se o ciclo normal do TDD.
Depois de terminar de implementar todos os testes identificados pela equipe, é hora de você pensar mais um pouco e tentar identificar o que ainda pode ser verificado, o que mais pode dar errado. Também nessa hora, será preciso pesar a relação custo/benefício dessas verificações.
Custo/benefício de uma verificação
Cada verificação feita no software é uma "garantia" que o mesmo funcionará como esperado. Quanto mais verificações o software fizer, supostamente mais estável e seguro ele será. Se os testes verificam à logica do negócio, suas regras, variações e cenários, mais próximo de cumprir as exigências do projeto ele estará.
Verificar "100%" do software, cobrindo todas as variações possíveis, é, então, a saída para a construção de um software de alta qualidade, certo ? Com certeza não.
Cada verificação adicionada no software tem um impacto diferente sobre sua qualidade. E há um custo de esforço e tempo em cada uma delas.
Pensando no Princípio de Pareto, que diz que 80% dos efeitos vem de 20% das causas, quanto mais nos aproximarmos dos 20% principais, maiores serão os benefícios alcançados pelos testes.
Assim, é importante que os testes sejam concentrados no que irá gerar diferencial para seu usuário - que no caso serão os casos levantados pelo Teste de Aceitação e possivelmente o brainstorming feito junto à equipe de desenvolvimento.
Para o resto, o esforço e custo são alto demais comparados aos benefícios trazidos. Assim, eles podem ser descartados.
Existem, claro, projetos que precisam de uma cobertura de 100% dos testes, como aqueles desenvolvidos para aplicações críticas que envolvam risco à segurança ou saúde, como por exemplo um software para controle de um avião. Mas se você não desenvolve software para esses nichos, é produtivo concentrar esforços na regra 80-20.
Não perca tempo com certos testes
Um erro comum que volta e meia vejo pessoas cometendo, principalmente aquelas que estão iniciando a prática do TDD, é o de verificar coisas que trarão pouqüíssimos benefícios e que tomam tempo considerável. Por exemplo:
- TEST( DefineNumeroCorretamente )
- {
- ContaBancaria conta;
- conta.DefinirNumero( 1234 );
- CHECK_EQUALS( 1234, conta.Numero() );
- }
O código acima testa se o número da conta ficou com o valor correto após definí-lo. É absolutamente ineficiente fazer testes como este, que não trarão benefícios relevantes e desperdiçarão tempo do desenvolvedor (pense, por exemplo, em uma classe com 10 atributos...).
Teste o que for relevante
É muito mais importante, por exemplo, checar estados (valores) com alguma lógica ou processamento relacionado:
- TEST( ContaTransfereCorretamente )
- {
- ContaBancaria minhaConta, suaConta;
- minhaConta.DefinirSaldo( 1000.00 );
- suaConta.DefinirSaldo( 0.00 );
- minhaConta.Transferir( suaConta, 400.00 );
- CHECK_EQUALS( 400.00, suaConta.Saldo() );
- CHECK_EQUALS( 600.00, minhaConta.Saldo() );
- }
Verificar as mudanças de comportamento relacionados aos estados são freqüentemente os alvos dos testes importantes:
- TEST( NaoConsegueEfetuarSaqueQuandoContaEstaSemFundos )
- {
- ContaBancaria conta;
- conta.DefinirSaldo( 0.00 );
- // Verifica se lanca a excecao
- // ESaldoContaInsuficiente caso nao
- // consiga efetuar o saque
- ASSERT_THROW(
- conta.EfetuarSaque( 10.00 ),
- ESaldoContaInsuficiente
- );
- }
E, mais ainda, a interação entre os objetos:
- TEST( CartaoDebitoNaoConsegueComprarCasoAContaNaoTenhaFundos )
- {
- ContaBancaria conta;
- CartaoDebito cartao( conta );
- conta.DefinirSaldo( 0.00 );
- Compra *compra = GerarCompraFicticiaNoValorTotalDe( 10.00 );
- // Verifica se lanca a excecao
- // ESaldoContaInsuficiente caso nao
- // consiga efetuar a compra
- ASSERT_THROW(
- compra->PagarCom( cartao ),
- ESaldoContaInsuficiente
- );
- delete compra; // Nao deve ser executado
- }
A maioria dos testes importantes verificam a interação entre objetos. Outros, o comportamento de cada objeto e, alguns, seu estado.
Concluindo
Quem: O usuário define o que será verificado. A equipe acrescenta outras verificações importantes. Você acrescenta ainda o que julgar necessário.
Onde: As verificações necessárias podem ser registradas em diversos tipos de documento, como num cartão onde é descrita a estória do que será implementado, num documento de Teste de Aceitação ou mesmo num detalhamento da Espeficiação de Requisitos. O importante é que estas informações estejam disponíveis ao construir o software.
Quando: Quando a relação custo/benefício for satisfatória. Procura-se testar os 20% que darão os 80% dos benefícios.
O Que: Teste a interação dos objetos principalmente, seu comportamento e por fim seus estados.
Como: Seguindo o ciclo do TDD e criando testes com frameworks como CppUnit (mostrado acima) ou MockPP. Aquele que estiver disponível para sua linguagem de programação e tenha a facilidade de uso e a flexibilidade requerida para o desenvolvimento dos testes do seu software.
Nenhum comentário:
Postar um comentário