22 agosto 2007

Comece escrevendo testes automáticos

Muitas empresas ainda tem receio de experimentar metodologias ágeis, seja pela carga semântica herdada no nome extreme programming, seja por falta de informação ou orientação profissional confiável. (Claro, há muito para se discutir a respeito desse “confiável”.)

Começar a utilizar metodologias ágeis pode ser mais fácil do que se imagina. Isso porque a maioria das práticas pode ser adotada isoladamente, e a qualquer tempo em um projeto, resultando em benefícios claros e rápidos.

Diferentemente dos modelos tradicionais de processos de software, essas metodologias entendem que a adoção de novas práticas e processos em uma organização deve ser feita de forma gradativa. Nenhum processo complexo será efetivamente absorvido se implantado todo ele de uma só vez.

A ordem com que as práticas devem ser adotadas se orienta pelas necessidades e prioridades naturais de cada equipe, em cada projeto, e não por roteiros cartesianos definidos em modelos ou exigidos por certificações.

O lado negativo desse cenário flexível é que muitos líderes de equipe acabam não adotando prática nenhuma, por não saberem por onde começar. Preceber com clareza qual ponto do processo precisa ser otimizado com maior prioridade não é uma tarefa muito simples, pelo menos quando não se está acostumado a analisar as coisas dessa forma.

O intuito desse artigo é o de sugerir a prática de escrita de testes automáticos como ponto de partida para equipes que já se perceberam sintonizadas com os valores e princípios ágeis, mas que ainda não sabem por onde começar. Vale também para aqueles que sentem vontade de experimentar alguns conceitos ágeis, mas não têm coragem, por não conseguirem visualizar de antemão o impacto que isso poderia causar.

Na prática, não há muita diferença se seu projeto é reconhecido ou não como um projeto Scrum legítimo ou XP legítmo. Não há, pelo menos no intuito desse artigo, onde se discute a melhoria efetiva do processo. Não estamos tratando aqui (ainda) de aspectos comerciais ou mercadológicos porventura envolvidos.

Então não interessa muito se alguém acha que, ao aplicar apenas uma prática do XP, você não está fazendo XP. O que interessa é que essa prática melhore seu processo de desenvolvimento em algum sentido, em algum grau, por menor que seja.

Ao aplicar uma prática – inicialmente de forma tímida e experimental, e depois de forma entusiasmada e sistemática – ganha-se confiança para então experimentar alguma outra. É assim que se aprende a praticar uma nova metodologia: aos poucos.

O fato é que a adoção da prática de programação orientada a teses também pode ser feita em vários níveis de profundidade. Se por um lado é uma disciplina ampla, variada em pontos de vista, técnicas e dialetos, por outro se baseia em princípios simples que podem ser muito bem expressos em uma suite de testes relativamente despretenciosa e mesmo incompleta.

Mesmo que seu sistema já possua uma quantidade enorme de funcionalidades desprovidas de testes automáticos, escrever o primeiro teste trará grandes benefícios. É preferível ter parte do código testada a não ter nada. O mais importante, no inicio, é ter uma suite que seja simples e consiga conquistar a simpatia e a confiança da equipe. No desenvolvimento ágil, sempre haverão oportunidades para melhorar a forma com que se escreve os testes. No início, meu conselho é: escreva testes simples.

Segundo esse princípio, fica claro que começar a praticar a disciplina de testes automáticos possui impacto muito pequeno e praticamente nenhum risco. Afinal de contas, basta escrever o primeiro teste, da forma mais simples possível ;-)

É óbvio que a prática envolve mais do que a simples automação dos testes. Bem mais. Há muito pra se aprender e evoluir dentro dessa disciplina, mas para isso é preciso começar.


O que é um teste automático?


Um teste automático é um programa que verifica se um outro programa está funcionando conforme o esperado.

Para entender a utilidade de se automatizar testes, faço uma analogia: para que serve a prova real, na matemática? (Alguns vão preferir lembrar da velha prova dos nove) Para se ter mais segurança a respeito da resposta encontrada.

Chama-se isso de princípio da redundância: quando alguma coisa não pode falhar de jeito nenhum, é preciso verificar seu perfeito funcionamento por, pelo menos, duas vias diferentes.

Nesse sentido, podemos apresentar novamente a definição, com outras palavras: um teste automático é a prova de que uma funcionalidade está corretamente implementada. Assim como se exigiria que um contador nos provasse o resultado final de um balancete, espera-se que um programador “demonstre” que a funcionalidade está corretamente implementada.

No desenvolvimento de software, esse conceito se torna ainda mais amplo, pois uma suite de testes verifica também que o software continuará funcionando corretamente ao longo do tempo, enquanto forem surgindo mudanças no código e nos requisitos.


Um exemplo prático

Vamos utilizar como exemplo o código de uma ferramenta que ajudei a escrever: o Coral . Trata-se de uma aplicação wiki, derivada do genial TiddlyWiki, cujo código java que inserimos tem, por enquanto, a única função de armazenar os conteúdos em banco de dados, em um servidor.

A arquitetura dessa aplicação é bem comum: sistema web, escritao em java, arquitetura em três camadas: Apresentação 100% javascript (a interface é o TiddlyWiki), com chamadas Ajax (utilizamos o DWR) a objetos de negócio, ou serviços, sem estado (stateless POJO's), que por sua vez chamam objetos de acesso a dados (DAO's) baseados em Hibernate para gravar os dados. Tudo isso é integrado com Spring:



Quanto ao domínio da aplicação, é bem simples: o sistema possui uma única entidade chamada Tiddler, que representa um tópico do wiki. Esses tópicos são organizados em namespaces, que representam um tema em particular (um conjunto de páginas relacionadas).

Vamos dar uma olhada em um dos teste do Coral:


public void testLoadTiddler() {

// cria um novo Tiddler
Tiddler t = getTestTiddler();

// Salva no BD
TiddlerService service = (TiddlerService) getBean("tiddlerService");

service.saveUpdateDeleteTiddlers("main", new Tiddler[]{t}, new String[0], new Date());

// Lê novamente o Tiddler
t = service.loadTiddler("main", "TestTiddler");

// Verifica os dados salvos
assertEquals("main", t.getNamespace());
assertEquals("TestTiddler", t.getTitle());
assertEquals("this is a [[Tiddler]] that im using to test.", t.getEncodedText());
assertEquals("welcome test", t.getTagsString());
assertEquals("200607132248", t.getCreatedString());
assertEquals("200607132249", t.getModifiedString());
assertEquals("BrunoPedroso", t.getModifier());

// Verifica se o método retorna null para tiddlers ou namespaces inexistentes

assertNull(service.loadTiddler("namespaceInexistente", "TestTiddler"));
assertNull(service.loadTiddler("TestNamespace", "TiddlerInexistente"));

}


Na prática, um teste automático é um método comum que faz três coisas:

1. Preparação;
2. Exercício;
3. Verificação;

Antes de testar, é preciso preparar o sistema, deixando-o na situação necessária para se realizar um dado caso de teste. No nosso exemplo, o método testLoadTiddler() apenas cria um Tiddler no banco de dados:


Tiddler t = getTestTiddler(); // esse método cria o Tiddler com dados de teste...
(...)
service.saveUpdateDeleteTiddlers("main", new Tiddler[]{t}, new String[0], new Date());


(Não se assuste com a assinatura do método saveUpdateDeleteTiddlers() por enquanto. Nesse exemplo, ele apenas salva um Tiddler no banco...)

Em seguida, o método de testes executa as operações que pretende testar. Nesse exemplo, o objetivo do teste é verificar se o método loadTiddler() da classe TiddlerService funciona corretamente.


t = service.loadTiddler("main", "TestTiddler");


Para ter certeza de que esse método funciona corretamente, precisamos verificar o objeto retornado pelo método. Como fomos nós que criamos o tiddler que estava no banco de dados (na fase de preparação), sabemos quais dados devem estar gravados lá. Essa verificação é feita com métodos assertXXX() da API do JUnit:

assertEquals("main", t.getNamespace());
assertEquals("TestTiddler", t.getTitle());
assertEquals("this is a [[Tiddler]] that im using to test.", t.getEncodedText());
assertEquals("welcome test", t.getTagsString());
assertEquals("200607132248", t.getCreatedString());
assertEquals("200607132249", t.getModifiedString());
assertEquals("BrunoPedroso", t.getModifier());

assertNull(service.loadTiddler("namespaceInexistente", "TestTiddler"));
assertNull(service.loadTiddler("TestNamespace", "TiddlerInexistente"));

Em ferramentas IDE como o Eclipse, uma suite de testes funciona, para o programador, mais ou menos como o verificador de código que sublinha trechos do código sintaticamente errados enquanto se escreve o código.


A diferença é que a suite de testes automáticos verifica erros de semântica, enquanto o compilador verifica apenas os erros de sintaxe.

Cada vez que o programador escreve algum código, e precisa verificar o resultado dos testes, basta apertar um botão e executá-los:



Para os testes que não “passam”, é possível verificar a causa e abrir qualquer trecho de código envolvido, simplesmente usando o duplo-clique no elemento desejado da pilha de erro.



Apenas um exemplo

O exemplo apresentado serve apenas para se ter um primeiro contato com a automação de testes. Sugiro que se leia os outros testes que escrevemos para o Coral. Há outros testes mais interessantes lá. Embora não sejam muitos, já servem para dar uma boa idéia da abordagem que adotamos. Melhor ainda seria baixar o código e executá-lo em sua máquina. (Aqui explica como. )

Existem outros detalhes que poderiam ser explicados sobre a suite de testes do Coral. Como é uma suite que testa vários componentes de uma só vez (e não pequenas unidades, como sugere o nome “teste unitário”), precisa lidar com algumas outras questões – como a criação e execução do banco de dados, e a “montagem” dos componentes da aplicação – antes que os testes possam ser executados. (Essas “funções” são desempenhadas pela classe TestePadrao )
Outras abordagens

Embora outras abordagens possam ser mais eficientes ou completas, gosto muito dessa que testa, de uma só vez, vários componentes da aplicação. No caso do Coral, um único teste exercita serviço (objeto de negócio), DAO, e mapeamento Hibernate.



A primeira vantagem que vejo nessa abordagem é a de que se escreve menos testes. Com um esforço pequeno no inicio, é mais fácil convencer a equipe e a gerência a automatizar os testes.

Além disso, testes unitários costumam parecer inúteis para um programador que está começando. Ao escrever um teste para um método muito simples, cujo código se escreve mais rápido do que o próprio teste, o programador tem a impressão de que está perdendo tempo (é claro que não está), e acaba criando resitência com a prática.

Como os resultados da prática são mais visíveis após algumas semanas (quando se iniciam as mudanças no código), acho melhor começar com testes cuja utilidade é mais fácil de enxergar. Ao escrever um teste para um método de negócio complexo, envolvendo a integração de alguns componentes e acesso ao banco de dados, o programador vê mais nexo na prática, e se permite experimentá-la com mais boa vontade.

É claro que há a contrapartida: como um único teste exercita vários componentes, uma falha pode não apontar a causa do problema imediatamente, exigindo certo esforço de depuração. Entretanto, o simples fato de o teste detectar o erro já é benefício mais que suficiente para convencer os programadores da efetividade da prática.


Amadurecendo a prática

Depois que a equipe já está convencida dos benefícios de se automatizar os testes, é bem mais fácil abordar outras questões, como a granularidade, outras técnicas – como mocks – e o hábito de se escrever os testes antes do código.

Existem muitos outros artigos que abordam essas e outras questões relativas à prática de programação orientada a testes. A comunidade brasileira de agile tem produzido muito material bom a respeito e vale a pena conferir.

Listo aqui apenas alguns links de que me lembrei, para incitar a pesquisa mais detalhada. Esses artigos possuem links pra outros. Se lembrarem de mais algum, é só deixar um comentário.

Desenvolvimento orientado a testes
Introdução ao desenvolvimento orientado a testes
Andando de costas
Exemplo TDD, parte 1: Por onde começar

Falou!