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.

terça-feira, 16 de setembro de 2008

sábado, 6 de setembro de 2008

TDD na Prática - Parte III: Abstração, Simplificação e o Ciclo do TDD

Conhecer como verificar as partes do software e estabelecer o nível de abstração para obter os dados a serem verificados é um ponto vital quando se escreve código de teste.

Expor uma interface simples em uma classe e esconder seus detalhes de implementação é imprescindível para alcançar um modelo de fácil reutilização, substituição e que possa ser facilmente testado. Dependências entre classes devem ser tornadas explícitas e bem-definidas, de forma que se possa trocar a classe da qual se depende por outra, sem que haja um esforço adicional.

E, de novo, aplicando os conceitos de acoplamento e coesão e refatorando o código, podemos obter código com a qualidade desejada. A experiência conta muito a favor, é claro, mas tendo em mente a meta de sempre tentar simplificar as coisas ao máximo (KISS- Keep It Small and Simple), é possível chegar a resultados muito satisfatórios.

Só implementar o necessário

O interessante em o design seguir os testes é que você acaba percebendo a quantidade de código que deixa de escrever. Se você se concentra em escrever código que tente resolver um determinado problema da forma mais simples possível, muita coisa que talvez você fosse escrever, pensando que poderia ser útil futuramente, deixa de ser feita - justamente porque você acabou não precisando pra nada (YAGNI - You Ain't Gonna Need It).

Um teste de cada  vez

Às vezes quando estamos criando um teste, pensamos em uma outra coisa que pode ser testada, não exatamente relacionada à parte de código que está sendo testada, e que  podemos adicionar ao teste que está sendo feito agora. Resista a esta tentação. Mantenha cada funcionalidade que precisa ser testada em testes diferentes. Teste uma funcionalidade por vez. Os testes devem permanecer os menores e mais simples possíveis.

Não implemente mais do que o exigido pelo teste

Após escrever o código do teste e executá-lo, ele irá falhar. Afinal, você ainda não escreveu o código que o implementa. Então você começa escrever o código que implementa a funcionalidade e, de repente, bate aquela idéia de escrever um método ou função que você tem certeza absoluta que vai usar no próximo teste. Por que já não deixar pronto o código para o próximo teste, que você tem certeza que vai escrever ? Acontece que você está escrevendo somente o código necessário para passar no teste. Ao escrever um teste de cada vez, você deve implementar uma funcionalidade também a cada vez. Deixe a implementação sempre seguir os testes. Resista a vontade de escrever código sem que algum teste tenha sido escrito para o mesmo antes.

Refatore

Se o código que você irá escrever for muito parecido com algo que você já fez, refatore (DRY - Don't Repeat Yourself). Se você acha que determinado pedaço de código não faz parte da abstração criada (classe, método, função, etc.), refatore. Se um pedaço de código pode ser tornado mais simples e fácil de entender, refatore. Não deixe de aplicar este passo do ciclo. Ele gera um enorme impacto na qualidade e diminui muito o custo da manutenção futura do código.

E outra coisa importante: Refatore os testes também. O código de teste também pode ser simplificado e melhorado, como o código que o implementa.

Em suma, siga o ciclo do TDD

O ciclo do Test-Driven Development se baseia em 3 passos:

  1. Escreva um teste que verifique uma funcionalidade;
  2. Implemente somente o código para passar no teste;
  3. Refatore o código criado;

Respeitando o ciclo, você mantém o código sob controle, não disperdiça tempo criando algo que não vai precisar e mantém o escopo de cada teste sempre pequeno, facilitando seu desenvolvimento e manutenção.

Tente sempre seguir o ciclo. Não sucumba às tentações. ;)