sexta-feira, 26 de setembro de 2008

TDD na Prática - Parte IV: Quem, Onde, Quando, O Que e Como

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: