Cobertura de testes com Emma
Quando desenvolvemos testes automatizados para uma aplicação é importante avaliar se eles estão alcançando todas as partes do sistema. O Emma é uma ferramenta open source que ajuda nesta tarefa, fazendo a análise da cobertura de testes de um projeto Java e gerando um relatório em formato texto ou HTML. Esse relatório representa um feedback importante para os desenvolvedores, pois indica quais áreas do projeto ainda não estão sendo cobertas por testes automatizados e portanto devem ser tratadas prioritariamente.
Neste artigo veremos os conceitos fundamentais do Emma e como funciona a ferramenta. Também mostraremos como instalar e usar o Emma e como analisar a cobertura de testes em suas aplicações.
Exemplo de uso
Antes de descrever em detalhes o funcionamento do Emma, vamos ver através de um exemplo o tipo de resultado que ele pode nos prover. Para isso, iremos implementar uma pequena classe, contendo um único método. Esse método recebe um inteiro positivo e retorna o número correspondente na seqüência de Fibonacci. O exemplo pode ser acompanhado mesmo sem saber detalhes de como calcular os números de Fibonacci. Em todo o caso, a regra de geração desses números é simples:
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(n) = fibonacci(n-2) + fibonacci(n-1)
Se o método recebe um número maior que 1, o retorno é a soma de cada um dos números de fibonacci de seus dois antecessores. Por exemplo, fibonacci(10) = fibonacci(8) + fibonacci(9)
.
O código da classe que contém o método fibonacci()
encontra-se na Listagem 1 e a classe de teste correspondente está na Listagem 2. Mais adiante veremos como gerar o relatório do Emma indicando a cobertura de teste da classe, mas por enquanto você já pode ver o resultado da análise do Emma na Figura 1.
package jm; public class Matematica { public int fibonacci(int numero) { if (numero >= 0) { if (numero == 0) return 0; if (numero == 1) return 1; return fibonacci(numero-1) + fibonacci(numero-2); } throw new IllegalArgumentException(); } }
Listagem 1. Classe que contém o método fibonacci().
package jm; import junit.framework.TestCase; public class MatematicaTeste extends TestCase { Matematica matematica = new Matematica(); public void testFibonacciZero() { verificaFibonacci(0, 0); } public void testFibonacciUm() { verificaFibonacci(1, 1); } public void testFibonacciDois() { verificaFibonacci(1, 2); } public void testFibonacciTres() { verificaFibonacci(2, 3); } public void testFibonacciQuatro() { verificaFibonacci(3, 4); } public void testFibonacciCinco() { verificaFibonacci(5, 5); } public void testFibonacciSeis() { verificaFibonacci(8, 6); } public void testFibonacciParaNumeroInvalido() { try { matematica.fibonacci(-1); fail("Deveria ter lançado IllegalArgumentException"); } catch (IllegalArgumentException e) { assertTrue(true); } } private void verificaFibonacci(int resultadoEsperado, int valor) { assertEquals(resultadoEsperado, matematica.fibonacci(valor)); } }
Listagem 2. Classe de teste usada para verificar o funcionamento do método fibonacci().
Figura 1. Resumo da cobertura de testes do projeto inteiro.
No topo desse relatório, o Emma indica o percentual de cobertura das classes, métodos e blocos (explicados mais adiante). Por exemplo, a análise da cobertura de linhas indica 100% (6/6), significando que todas as linhas executáveis da classe Matemática
foram exercitadas pelos testes.
Abaixo dessa informação, o Emma apresenta um resumo da quantidade de pacotes no projeto, bem como arquivos, classes, métodos e linhas executáveis. Por último, o Emma mostra a análise dos pacotes do sistema, informando o percentual de cobertura das classes de cada um, bem como de seus respectivos métodos, blocos e linhas.
No relatório do Emma, é possível obter mais detalhes sobre a cobertura de cada pacote, clicando em seu nome. Nesse exemplo, ao clicar no pacote jm
o Emma nos apresenta mais informações sobre ele, como ilustrado na Figura 2. No topo aparece um resumo do percentual de cobertura das classes, métodos, blocos e linhas do pacote. Mais abaixo, esses mesmos percentuais são apresentados novamente, agrupados por classe. Nesse caso em particular, só há uma classe, para a qual o Emma indica 100% de cobertura.
Figura 2. Resumo da cobertura de testes do pacote jm
.
O Emma permite que você se aprofunde ainda mais na análise de cada classe da aplicação. Por exemplo, no relatório de um pacote, você pode clicar sobre o nome de cada classe para ver seu código, e assim visualizar que linhas estão sendo cobertas. No exemplo, se clicarmos em Matematica.java
, o Emma nos apresentará a tela ilustrada na Figura 3.
Figura 3. Análise de cobertura da classe Matematica.
No topo, vemos um resumo da cobertura do arquivo Matematica.java
como um todo. Logo abaixo, o Emma apresenta a cobertura por classes contidas no arquivo. Para cada classe são apresentados os percentuais de cobertura agrupados por métodos. Por fim, o Emma mostra o código fonte do arquivo e colore com verde todas as linhas que são plenamente exercitadas quando os testes são executados. Quando uma linha não é executada, o Emma a realça em vermelho; já quando a linha foi parcialmente executada a cor amarela é usada. A idéia de uma linha parcialmente executada é explorada com mais detalhes adiante.
Exemplo de projeto com deficiências de cobertura
Para compreender o que aconteceria se o projeto não estivesse 100% coberto pelos testes, iremos apagar o método de teste testeFibonacciParaNumeroInvalido()
apresentado na Listagem 2. Fazendo isso, o Emma nos mostraria o relatório ilustrado na Figura 4.
Figura 4. Resumo da cobertura de testes inferior a 100%.
Nessa nova análise, o Emma indica que todas as classes e métodos continuam sendo executados pelos testes, entretanto verifica-se que o percentual de cobertura de blocos e linhas foi reduzido para 87% e 83% respectivamente. Clicando-se sobre o pacote jm
, e depois sobre o nome da classe, somos levados mais uma vez ao código fonte, como ilustrado na Figura 5.
Figura 5. Cobertura parcial da classe Matematica.
O Emma marca em vermelho a linha com código que não foi executado em nenhum momento pelos testes. Essa é uma das informações mais úteis que os desenvolvedores podem obter dessa ferramenta, já que representa um feedback concreto sobre as partes da aplicação que não estão sendo testadas e, portanto, mais têm potencial de produzir erros quando o sistema estiver em produção.
Preparando o ambiente
Agora que já vimos o que o Emma pode fazer por nossos projetos, vamos mostrar como colocá-lo em funcionamento. O primeiro passo é fazer o download do Emma aqui. O segundo é configurar o Eclipse, que escolhemos como IDE para este artigo.
Criando o projeto e os diretórios
No Eclipse, crie um novo projeto Java chamado Fibonacci
e configure os diretórios onde serão armazenados os códigos fontes. Haverá dois diretórios, um para o código da aplicação (src
) e outro para os testes (srcTeste
). Para efetuar essa configuração, escolha Project|Properties
, clique na opção Java Build Path
no painel à esquerda e depois na aba Source
à direita. Depois clique no botão Add Folder
, digite src
e clique em Ok.
Nesse momento o Eclipse irá perguntar se você deseja direcionar os arquivos compilados para o diretório bin
. Aceite essa sugestão. Agora só falta adicionar o diretório srcTeste
. Clique novamente no botão Add Folder
, em seguida no botão Create New Folder
, e digite srcTeste
. Com isso, os diretórios onde serão armazenados os fontes estarão configurados.
Configurando o JUnit e o Emma
Para executar testes automatizados, usaremos o JUnit. Para isso, é necessário adicionar a biblioteca junit.jar
ao classpath
do projeto. Escolha Project|Properties
, clique na opção Java Build Path
no painel à esquerda e depois na aba Libraries
à direita. Clique no botão Add External JARs
e selecione o arquivo junit.jar
, que já vem com o Eclipse no subdiretório plugins/org.junit_3.8.1
(a versão pode variar).
Finalmente, descompacte o arquivo do Emma que você obteve, crie um diretório lib
no Eclipse e coloque nele os seguintes arquivos do Emma: emma.jar
e emma_ant.jar
. Para que o Eclipse reconheça esses arquivos, escolha a opção de menu File|Refresh
.
Adicionando os arquivos do projeto
Crie a classe Matematica
, clicando com o botão direito no diretório src
e escolhendo a opção New>Class
. Forneça o nome e defina o pacote como jm
. Copie para a classe o código apresentado na Listagem 1.
Agora adicione a classe de teste. Clique com o botão direito no diretório srcTeste
, escolha a opção New>Class
e dê à classe o nome MatematicaTeste
. Ela também deverá ser colocada no pacote jm
. Copie para a classe o código da Listagem 2. Após a execução desses passos, seu projeto deverá estar organizado como ilustrado na Figura 6. Há um arquivo adicional na figura, chamado build.xml
, que você adicionará ao projeto mais adiante.
Figura 6. Estrutura do projeto.
Agora você pode executar o JUnit para verificar se tudo está funcionando corretamente. Clique com o botão direito sobre a classe MatematicaTeste
e escolha Run As>JUnit Test
. O JUnit deverá apresentar um resultado semelhante ao que aparece na Figura 7.
Figura 7. Execução dos testes no JUnit.
Executando o Emma
É possível executar o Emma através da linha de comando ou através do Ant. O uso do Ant é o mais interessante, pois permite automatizar a análise de cobertura, e portanto executá-la facilmente diversas vezes ao longo do projeto.
No Eclipse, clique com o botão direito sobre o nome do projeto, selecione New>File
e digite build.xml
. Em seguida, coloque nesse arquivo o conteúdo da Listagem 3. Para executar esse buildfile, clique com o botão direito sobre o arquivo e escolha a opção Run As>Ant Build...
Na tela apresentada, selecione a aba Classpath
, clique em User Entries
e em seguida no botão Add JARs
. Selecione todos os jars do diretório lib
(emma.jar
e emma_ant.jar
). Agora clique no botão Add External JARs
e selecione o arquivo junit.jar
no subdiretório plugins/org.junit_3.8.1
. Clique em Ok
e posteriormente em Run
.
Listagem 3. Script do Ant para executar o Emma.
Após a execução do Ant, você encontrará um novo diretório no seu projeto chamado emma/relatorio
. Nele haverá um arquivo index.html
que você poderá abrir em seu navegador web para ver o resultado da análise.
Compreendendo o funcionamento do Emma
O Emma é uma ferramenta de cobertura puramente baseada em instrumentação de bytecode. Isso significa que ele lê os bytecodes de sua aplicação (nos arquivos .class
) e faz alterações nos mesmos, para que, além de executarem as instruções do aplicativo, também capturem informações para serem usadas na geração do relatório de cobertura. Para que você compreenda o funcionamento do Emma e o que significa instrumentação na prática, iremos explicar cada passo executado pelo script build.xml
, apresentado na Listagem 3.
A execução do build.xml
envolve os seguintes alvos (targets) do Ant, que são executados na seqüência indicada abaixo:
init
– Apaga o diretóriobin
e o recria novamente, com o objetivo de eliminar qualquer código previamente compilado.compilaSrc
– Compila apenas as classes da aplicação e coloca os arquivos.class
no diretóriobin
criado no alvo anterior.instrumenta
– Nesse alvo o Emma é acionado pela primeira vez. Ele lê os arquivos.class
armazenados no diretóriobin
, e os instrumenta, criando uma cópia deles em outro diretório. Nessa cópia, além dos bytecodes originais, outros são adicionados com o objetivo de apoiar o funcionamento do Emma. Na Figura 8, é possível acompanhar visualmente o que acontece. Note que após a instrumentação, o arquivo.class
instrumentado recebeu novas instruções (em vermelho), que serão importantes em outra etapa. Ainda neste alvo, o Emma gera um arquivo chamadometadado.emma
(os nomes dos arquivos gerados são definidos no buildfile). Esse arquivo contém informações importantes sobre a estrutura das classes instrumentadas, que serão usadas depois durante a criação do relatório em HTML.compilaTeste
– Agora que os bytecodes da aplicação já foram instrumentados, podemos compilar os testes. Preferimos esperar para compilá-los nesse momento para evitar que as próprias classes de testes sejam instrumentadas e acabem sendo mostradas no relatório do Emma. O relatório final do Emma deve, é claro, conter apenas informações sobre a cobertura das classes da aplicação.testa
– Esse é o momento no qual o JUnit é executado. Ele é direcionado para testar a aplicação usando as classes instrumentadas pelo Emma. Isso fará com que os códigos inseridos pelo Emma (em vermelho na Figura 8) também sejam executados. Estes códigos especiais gravam informações no arquivocobertura.emma
, as quais indicam precisamente que instruções do arquivo.class
foram exercitadas pelos testes. Ao final da execução do JUnit, o arquivocobertura.emma
passa a conter informações sobre todas as instruções da aplicação executadas pelo JUnit.emma
– Aqui ocorre a finalização do trabalho do Emma. A ferramenta gera o relatório em HTML, a partir das informações armazenadas anteriormente, nos arquivosmetadado.emma
ecobertura.emma
.
Figura 8. Funcionamento básico do Emma.
Esse modelo de funcionamento permite coletar informações sobre a cobertura de testes de forma simples, independentemente da infra-estrutura sendo utilizada. O Emma apenas demanda o uso de uma JVM e obtém estatísticas de cobertura de forma não-intrusiva, com apenas um pequeno overhead de performance, e agindo silenciosamente durante a execução dos testes.
Mais sobre o resultado da análise
A unidade fundamental de cobertura usada no Emma é o bloco básico ou simplesmente bloco. Todas as demais unidades são derivadas desta. Um bloco é uma seqüência de instruções em bytecode sem jumps. Em outras palavras, ele sempre executa como uma unidade atômica (na ausência de exceções). Um bloco é considerado coberto quando o controle alcança sua última instrução. Um bloco coberto é, portanto, garantido de ter executado sem falhas ao menos uma vez.
À medida que se cria lógicas condicionais no código Java (como em loops e blocos case ou if), são criados novos blocos. Assim, 100% de cobertura de blocos significa 100% de linhas executáveis cobertas.
Com relação à cobertura de linhas, o que o Emma faz é descobrir como os blocos mapeiam para as linhas de código Java. Então, para cada linha, o Emma determina se todos os blocos básicos pelos quais a linha é responsável foram cobertos (lembrando que os blocos básicos são seqüências de instruções em bytecode, portanto não há um mapeamento um-para-um entre essas instruções e linhas Java). Se todos esses blocos tiverem sido cobertos, a linha é considerada 100% coberta. Senão a cobertura será parcial, e o Emma colore a linha com amarelo.
Veja exemplos de cobertura parcial no código ilustrado na Figura 9 (extraída da documentação do Emma). Na linha 6, apenas um ramo da condicional é executado. Já na linha 8, a variável vk nunca é incrementada, portanto a linha não tem cobertura de 100%. Note ainda que, como o corpo do loop nem o construtor default são executados, as linhas 10 e 14 são marcadas em vermelho, indicando ausência de cobertura.
Figura 9. Exemplo de cobertura parcial.
No Emma, para que uma classe seja considerada coberta, é preciso antes que seja considerada executável. Uma classe executável é indicada como tendo sido coberta se tiver sido carregada e inicializada pela JVM. O Emma relata a cobertura de classes de modo que você possa descobrir as que parecem não ter sido "tocadas" pelos seus testes: elas podem ser código “morto” ou com necessidade de atenção por testes.
Finalmente, o Emma considera um método coberto quando o processamento tiver entrado nele (ou seja, se seu primeiro bloco tiver sido coberto). Verificar se o método completou a execução seria problemático: um dado método pode ter vários pontos de saída, e não fica claro qual desses caminhos devem ser considerados o "oficial".
Procurar por métodos que não tenham sido cobertos é uma boa técnica para detectar código “morto”, ou que necessite de mais atenção por parte dos testes.
Analisando a cobertura de suas aplicações
Talvez você esteja se perguntando como incorporar o uso do Emma aos seus projetos atuais. O procedimento é simples, e para exemplificá-lo mostraremos como utilizá-lo em conjunto com um projeto open source, que pode ser obtido facilmente na internet. Escolhemos o EasyMock, uma importante ferramenta para auxiliar na criação de mock objects (usados para apoiar a realização de testes unitários), mas qualquer outro projeto contendo testes unitários seria adequado.
Baixe o EasyMock aqui. Ao descompactar o download, temos acesso aos arquivos src.zip
e test.zip
, entre outros. Para usar o Emma, basta executar os seguintes passos:
- Crie uma cópia do projeto
Fibonacci
(para simplificar, vamos re-aproveitar parte da estrutura desse projeto). - Nessa cópia, apague todos os arquivos do diretório
src
. No lugar deles, coloque os códigos fontes do EasyMock, descompactando o arquivosrc.zip
. - Apague todos os arquivos do diretório
srcTeste
e no lugar deles coloque o conteúdo do arquivotest.zip
. - Finalmente, edite o arquivo
build.xml
, e substitua a linha<property name="suite.testes" value="jm.MatematicaTeste"/>
por<property name="suite.testes" value="org.easymock.tests.AllTests"/>
. - Execute o
build.xml
. O relatório gerado é ilustrado na Figura 10.
Figura 10. Análise de cobertura de uma ferramenta open source.
Como se vê, usando o script do Ant apresentado nesse artigo, é fácil utilizar o Emma em projetos previamente existentes. O que fizemos no caso do EasyMock foi basicamente configurar os diretórios com os códigos fontes apropriados e, no arquivo build.xml
, definir na propriedade suite.testes
o nome da suite que contém todos os testes da aplicação (org.easymock.tests.AllTests
).
Para usar o Emma em seu projeto, você pode seguir um processo similar. Configure seus arquivos de código fonte e de código de testes nos diretórios sugeridos, e faça o script do Ant apontar para a sua suite de testes.
Conclusões
Emma é uma ferramenta que indica as partes do código fonte de uma aplicação que estão sendo exercitadas por testes automatizados. É útil, entre outras coisas, para detectar partes da aplicação que não estejam sendo testadas adequadamente ou simplesmente não estão sendo usadas. Esse tipo de feedback pode ser usado para ajudar o desenvolvedor a priorizar novos testes. O Emma usa instrumentação de bytecode para coletar informações sobre cobertura de forma silenciosa, simples e com baixo overhead de desempenho.
Aproveite e veja como facilitar a automação de testes de unidade usando mock objects.
Download do Exemplo
Baixe os fontes do exemplo de Fibonacci aqui.
Autoria
Texto de Vinícius Manhães Teles.
Ilustrações de Leandro Mello.
Publicado em 15/10/2006.
Licenciado como Creative Commons Atribuição.