Cobertura de testes com Rcov

Durante o desenvolvimento de uma aplicação usando testes automatizados é importante avaliar se nossos testes estão passando por todos os trechos do nosso código. O Rcov é uma ferramenta para analise de cobertura de testes usada em Ruby que gera um relatório em HTML indicando por onde nossos testes não estão passando e devemos melhorar.

Essa analise de cobertura é especialmente importante em Ruby pois por ser uma linguagem interpretada não contamos com o auxilio do compilador e facilmente podemos introduzir erros no nosso código e se eles não forem exercitados pelos nossos testes podem passar desapercebidos e só serem notados em produção.

Exemplo

Antes de mostrar em mais detalhes o uso do Rcov veremos um pequeno exemplo de relatório gerado pelo Rcov e como interpretar os seus dados. No nosso exemplo iremos implementar uma classe com um método estático que calcula o fatorial de um número que é definido por:

Figura 1. Formula de Calculo do fatorial
Figura 1. Formula de calculo do fatorial.

Esta definição implica, em particular, que 0! = 1.

Exemplo: 5! = 1 × 2 × 3 × 4 × 5 = 120

Na listagem 1 temos o código da nossa classe fatorial e na listagem 2 a sua respectiva classe de teste.


class Fatorial
  def self.calcula(numero)
    if numero < 0
      raise RangeError
    else
      return 1 if numero == 0
      return 1 if numero == 1
      return numero * calcula(numero - 1)
    end
  end
end

Listagem 1. Classe fatorial.


    require File.dirname(__FILE__) + '/../test_helper'
    class FatorialTest < Test::Unit::TestCase
    
      def test_fatorial_de_zero
        verifica_fatorial 1, 0
      end
    
      def test_fatorial_de_um
        verifica_fatorial 1, 1
      end
      
      def test_fatorial_de_dois
        verifica_fatorial 2, 2
      end
      
      def test_fatorial_de_tres
        verifica_fatorial 6, 3
      end
      
      def test_fatorial_de_quatro
        verifica_fatorial 24, 4
      end
      
      def test_fatorial_de_cindo
        verifica_fatorial 120, 5 
      end
      
      def test_fatorial_numero_negativo
        assert_raise(RangeError) do
             Fatorial.calcula(-1)
          end
      end
      
      private
      def verifica_fatorial(resultado, numero)
        assert_equal resultado, Fatorial.calcula(numero)
      end
      
    end

Listagem 2. Classe de teste.

Figura 2. Resumo da cobertura de testes do projeto
Figura 2 Resumo da cobertura de testes do projeto.

Na figura 2 temos um exemplo de relatório gerado pelo Rcov. Na primeira linha da tabela do Rcov temos as informações globais do projeto, indicando numero de linhas, linhas de códigos e os percentuais de cobertura. Nas demais linhas temos as mesmas informações para cada classe ou módulo do projeto. O relatório gerado pelo Rcov nos permite visualizar informações mais detalhadas sobre a cobertura de cada uma das nossas classes bastando para isso clicar no nome da classe. Na figura 3 vemos os detalhes da nossa classe Fatorial.

Figura 3. Detalhamento da cobertura da classe Fatorial
Figura 3 Detalhamento da cobertura da classe Fatorial.

Projeto com deficiências de cobertura

Para exemplificar um projeto com deficiência de cobertura iremos remover o teste test_fatorial_numero_negativo. Com essa remoção o Rcov nos dá um novo relatório ilustrado na figura 4.

Figura 4. Resumo do projeto com deficiência de cobertura
Figura 4 Resumo do projeto com deficiência de cobertura.

Nessa nova analise o relatório sinaliza a queda na cobertura de testes e indica quais as classes que estão com a cobertura prejudicada. Clicando-se na nossa classe Fatorial iremos para a analise detalhada da classe ilustrada na figura 5.

Figura 5. Detalhamento da cobertura da classe Fatorial
Figura 5 Detalhamento da cobertura da classe Fatorial.

O Rcov sinaliza em vermelho as linhas que não foram exercitadas pelos nossos testes dando um feedback imediato ao desenvolvedor de que parte do sistema ele não testou corretamente e tem maiores chances de esconder erros.

Instalando o Rcov

Existem algumas formas de se instalar o Rcov.

1 - Usando o RubyGems

gem install rcov

2 - Através do tarball

Após baixar a ultima versão no site do Rcov basta executar:

ruby setup.rb

3 - Atraves do apt.

Se você estiver utilizando Debian ou Ubuntu você pode fazer essa instalação executando:

aptitude install rcov

Vale lembrar que em todas as formas de instalação citadas devemos instalar utilizando uma conta de administrador do sistema.

Usando o Rcov

Após a instalação do Rcov o seu computador está pronto para gerar os seus relatórios de cobertura bastando para isso executar rcov test/*.rb. Esse comando irá executar todos os .rb dentro do diretório test e gerar os relatórios de cobertura dentro da pasta coverage. Essa execução irá incluir nos relatórios algumas informações que não são referentes ao nosso projeto e provavelmente não interessantes. Na figura 6 ilustramos essas informações desnecessárias.

Figura 6. Detalhamento da cobertura com informações desnecessárias
Figura 6 Detalhamento da cobertura com informações desnecessárias.

Nesse exemplo temos dois tipos de informações desnecessárias: standard & site_ruby libraries. Para remover essas informações bastaria modificar a execução para rcov -x /site_ruby/ --rails test/*.rb, onde a opção --rails não incluem as configurações do framework e a opção -x permite excluir de acordo com uma expressão regular. Para maiores detalhes basta executar rcov -h

Automatizando o uso do Rcov em um projeto Rails

Como em XP estamos sempre buscando automatizar tudo que é possível iremos fazer isso com nossa analise de cobertura. Para essa automatização iremos criar uma task do Rake em lib/rcov.rake


    require 'rake/clean'
    
    RCOV_OUT = "public/coverage"
    
    EXCLUDE = "-x /site_ruby/"
    
    CLOBBER.include(RCOV_OUT)
    
    RCOV = "rcov --no-color"
    
    task :coverage_units do
        sh "find test/unit -name '*.rb' \
            | xargs #{RCOV} #{EXCLUDE} --rails --output #{RCOV_OUT}/units"
    end
    
    task :coverage_functional do
        sh "find test/functional -name '*.rb' \
            | xargs #{RCOV} #{EXCLUDE} --rails --output #{RCOV_OUT}/functionals"
    end
    
    task :coverage_all do
        sh "find test/* -name '*.rb' \
        | xargs #{RCOV} #{EXCLUDE} --rails --output #{RCOV_OUT}/all"
    end
    
    task :coverage => [:coverage_all]

Listagem 3. Task para automatizar a geração dos relatórios de cobertura.

Para executa-la basta utilizar o comando rake coverage

Essa nossa task utiliza comandos exclusivos do unix e precisa ser modificada para funcionar no Windows.

Existe uma sugestão de implementação em windows no blog asplake.

Tornando a cobertura de testes obrigatória

Seguindo as boas práticas de integração continua, não podemos ter o nosso repositório com testes quebrados e por isso iremos criar um teste unitário que quebre caso nossa cobertura não esteja em 100%. Essa pratica de manter a cobertura sempre em 100% no projeto está diretamente ligada com o conceito de não deixar janelas quebradas. O procedimento é criar um novo teste unitário para validar a cobertura conforme exibido na listagem 4.


    require File.dirname(__FILE__) + '/../test_helper'
    require 'test/unit'
    require 'hpricot'
    require 'open-uri'
    
    class CoverageTest < Test::Unit::TestCase
      
      XPATH = "/html/body/table/tbody/tr:eq(0)/td:eq(3)/table/tr/td:eq(0)/tt"
      
      def test_if_application_is_fully_covered
        content = [] 
        File.open('public/coverage/all/index.html', "r") do |file|
          file.each { |line| content << line }
        end
        doc = Hpricot(content.to_s)
        assert_equal "100.0%", doc.search(XPATH).to_html
      end
      
    end 

Depois devemos modificar nosso arquivo do Rake para excluir esse teste da analise de cobertura. No código da listagem 3 devemos substituir o trecho -name '*.rb' por -name '*.rb' \! -name 'coverage_test.rb' nas tasks coverage_functional e coverage_all.

Listagem 4. Teste unitário que valida se nossa aplicação está com cobertura em 100%.

Nesse nosso teste vamos no HTML gerado para verificar a cobertura e para isso utilizamos uma ferramenta de parser de HTML chamada hpricot que nos permite utilizar xpath.

As armadilhas da cobertura.

É notório que a analise de cobertura ajuda muito mas não deve ser analisada de forma isolada pois em algumas situações ela pode enganar o desenvolvedor e deixar passar algum erro. Vamos a exemplos.

Exemplo:

Modificando a implementação da nossa classe Fatorial e removendo-se o método test_fatorial_numero_negativo vemos que a nossa cobertura permanece em 100% embora não tenhamos nenhum teste para número negativo. Isso acontece pois o Rcov analisa as linhas por onde o código passou e efetivamente ele passou pela linha raise RangeError if numero < 0 em todos os testes mas nenhum deles respondeu com sucesso a operação lógica numero < 0.


    class Fatorial
      def self.calcula(numero)
          raise RangeError if numero < 0
          return 1 if numero == 0
          return 1 if numero == 1
          return numero * calcula(numero - 1)
      end
    end

Listagem 5. Classe modificada para provocar falha na cobertura.

Figura 7. Relatório de falsa cobertura
Figura 7 Relatório de falsa cobertura.

Autoria

Texto de Vinícius Manhães Teles.
Ilustrações de Leandro Mello.

Publicado em 14/05/2007.

Licenciado como Creative Commons Atribuição.