Testes de mutantes: quem vigia os vigilantes?

Quis custodiet ipsos custodes? – Decimus Junius Juvelanis

A frase acima, do poeta romano Juvelanis, é geralmente traduzida como: “Quem vigia os vigilantes?“. O contexto original era de conflitos conjugais, mas comumente é usada para questionar o abuso de poder e corrupção no Estado.

Mas o que isso tem a ver com desenvolvimento de software? Os vigilantes do nosso código são os testes automatizados. Mas quem testa os testes automatizados? Ou, de maneira mais clara: como saber se nossos testes estão realmente detectando falhas no nosso código?

Um pequeno exemplo

Assuma que há uma classe CalculadoraDeImposto que, para Produtos com preço maior que 500, calcula um imposto de 10%. Produtos com preço inferior são isentos.

public class CalculadoraDeImposto {
    private static final BigDecimal ALIQUOTA = new BigDecimal("0.10");

    public BigDecimal calcula(Produto m) {
        if(m.getPreco().compareTo(new BigDecimal("500")) > 0) {
            return ALIQUOTA.multiply(m.getPreco());
        }
        return BigDecimal.ZERO;
    }
}

Criamos dois testes de unidade automatizados: um teste para um produto não isento e outro para um isento.

public class CalculadoraDeImpostoTest {
    private CalculadoraDeImposto calculadoraCPMF;

    @Before
    public void setUp(){
        calculadoraCPMF = new CalculadoraDeImposto();
    }

    @Test
    public void precoAcimaDoLimite() {
        Produto iphone = new Produto(new BigDecimal("1000"));
        calculadoraCPMF.calcula(iphone);
    }

    @Test
    public void produtoIsento() {
        Produto pendrive = new Produto(new BigDecimal("200"));
        calculadoraCPMF.calcula(pendrive);
    }
}

Será que esses testes são suficientes? Estão testando todos os cenários relevantes? Capturam possíveis falhas ao modificarmos o código de produção?

Cobertura descoberta

Uma ideia é rodar todos os testes automatizados, inspecionando a execução e marcando quais partes do código de produção são invocadas. Assim, podemos listar os trechos que NÃO foram “exercitados” pela suíte de testes.

Bibliotecas que implementam esse tipo de ideia são chamadas de ferramentas de cobertura de código. No mundo Java, temos ferramentas bastante usadas como JaCoCo, Cobertura e JCov.

A ferramenta JaCoCo possui o ECL Emma, um plugin do Eclipse, e oferece várias medidas de cobertura: a % de classes (type coverage), a % de métodos (method coverage), a % de linhas de código (line coverage), a % de ifs e switchs (branch coverage), e a % de instruções no byte code (instruction coverage) invocadas pelos testes. Além disso, há uma medida baseada na complexidade ciclomática (complexity coverage).

O que estamos verificando é apenas se algum teste invoca cada trecho do código de produção. Mas será o bastante para verificarmos a qualidade dos nossos testes?

Vejamos. Para nosso exemplo acima, a cobertura relatada pelo ECL Emma é 100% para todas essas medidas de cobertura. Poxa, mandamos bem nos testes!

Mas, peraí… Repare novamente nos dois testes de unidade acima: não há nenhum assert!

Mutantes: a evolução

Outra ideia é inserir pequenos defeitos propositalmente no nosso código de produção e executar a suíte de testes, observando se algum teste falha.

Cada defeito inserido é chamado de mutação e cada nova versão do código de produção que possui uma mutação é chamada de mutante. Se a algum teste falhar, o mutante é “morto”. Mas se nenhum teste falhar, o mutante permanece “vivo”. A % de mutantes mortos, que foram detectados por alguma falha nos testes, é uma medida da qualidade dos seus testes.

As bibliotecas que implementam essa ideia são chamadas de ferramentas de testes de mutantes. No mundo Java, temos ferramentas como Jester, Jumble e muJava e PIT, a mais ativamente desenvolvida.

O PIT possui uma lista de operadores de mutação, que são templates de defeitos possíveis que podem ser inseridos no código de produção. Alguns exemplos:

  • Changed conditional boundary: mudar if(a < b) para if(a <= b)
  • Negated conditional: inverter if(a == b) para if(a != b)
  • Removed conditional: Forçar if(true)
  • Removed call to method: retirar chamada de algum método
  • Mutated return: trocar retorno por null

Ao todo, há 25 operadores de mutação mas por padrão são executados apenas 7 desses. É possível configurar quais serão usados.

Esses operadores de mutação são aplicados ao código de produção através de ferramentas de manipulação de bytecode e a API de instrumentação do Java, gerando novas versões do código (os mutantes). Os testes que são afetados por um novo mutante são selecionados usando ferramentas de cobertura de código. Então, os testes são executados e são verificados os mutantes sobreviventes, que não ocasionaram falha em nenhum teste.

O PIT pode ser disparado pela linha de comando e também pelo Ant, Maven e Gradle. Há também plugins para Eclipse e IntelliJ. Além disso, existe um plugin que gera relatórios para o Sonar.

Executando nossos testes anteriores a partir do PIT, foram gerados 14 mutantes para as classes CalculadoraDeImposto e Produto. Desses, apenas 5 foram detectados (mortos) pelos testes e 9 sobreviveram. Chegamos a 36% de cobertura de mutantes.

Veja o relatório de mutantes do PIT exibido no Eclipse:

Relatório do PIT no Eclipse

Há também um relatório que mostra os mutantes gerados a partir de cada linha do código de produção, destacando os mortos e sobreviventes. Abaixo, perceba que foram gerados vários mutantes para a classe CalculadorDeImposto. Para a linha 10, que possui um if, foram gerados 7 mutantes, com 5 sobreviventes:

SUmário dos mutantes para a classe CalculadoraDeImposto

Para aumentarmos a cobertura de mutantes, teríamos de fazer asserts e, provavelmente, mais testes.

Nem tudo é perfeito

Executei o PIT para alguns projetos reais em uma máquina Core i7 com 16 GB de RAM.

O projeto A, um pequeno conversor de formatos de arquivos, possui apenas 30 classes de código de produção, com 352 linhas ao todo. Tem 126 testes de unidade, que foram executados pelo JUnit em 0,17s. O ECL Emma calculou 81,2% de line coverage e 80,6% de branch coverage, levando 0,176s.

Foram calculados 77% de cobertura de mutantes, gerados utilizando os 7 operadores de mutação padrão do PIT. A espera foi de 11s ou 62x os testes de cobertura. Com todos os 25 operadores de mutação do PIT, o resultado foi 75%. A demora foi de 21s, superando em 119x o ECL Emma!

Já o projeto B, uma aplicação Web de médio porte com 689 classes de produção, totaliza 16435 linhas de código. Há, no mesmo source folder, 829 testes de unidade e de DAO que levaram 24,4s para serem executados pelo JUnit. Os 39,1% de line coverage e 30,8% de branch coverage foram calculados pelo ECL Emma em 30s.

Os 7 operadores de mutação padrão do PIT levaram a uma cobertura de mutantes de 14%, tomando 33min 39s. São 67x o tempo do ECL Emma. Quando utilizados todos os 25 operadores do PIT, o valor da cobertura de mutantes passou a 24%. O interessante é que mais dos novos mutantes foram pegos pelos testes, aumentando a porcentagem. O tempo gasto foi de 2h 43min 2s ou 326x o tempo do ECL Emma!

Um outro projeto C, uma aplicação Web de maior porte, com 948 classes e 27552 linhas de código, tomou 12min e 3s para que o JUnit executasse seus 5074 testes de unidade e de DAOs. O ECL Emma calculou uma line coverage de 63,4% e branch coverage de 57,7% em 13min 39s. Pela proporção os mutantes padrão do PIT levariam de 14h a 16h e todos os mutantes, em torno de 3 dias! Confesso que não tive paciência de executar o PIT nesse projeto…

Perceba o sério problema: testes de mutantes demoram muito! Parece haver uma explosão combinatorial à medida que temos mais operadores de mutação a serem aplicados, mais código de produção a ser mutado, mais testes de unidade/integração a serem executados.

A ideia de testes de mutantes remete à decada de 70, mas a complexidade computacional do problema fez com que só agora ferramentas reais possam ser usadas na prática. Mas, se for utilizá-las, prepare-se para esperar!

Tags: ,

11 Comentários

  1. Rafael Ponte 17/08/2016 at 23:59 #

    Excelente artigo, Alexandre.

    Na minha opinião o teste valida o código e o código valida o teste. Quando lido com testes que estão passando e que tenho alguma dúvida das asserções feitas eu começo a mudar o código de produção, seja mudando um if ou condicional ou mesmo removendo a chamada de uma método; dai espero que o teste quebre. Se não quebrar já sei que tem algo bem errado.

    Faço isso com mais frequência quando lido com mocks na qual dou ou verifico comportamentos, assim percebo melhor a qualidade dos testes. Não chega a ser um PIT, mas ajuda no dia a dia.

    Enfim, gostei muito da ferramenta, pena ele demorar tanto. Talvez deixando o trabalho dele pro servidor de CI as coisas comecem a valer a pena e fazer mais sentido. O que acha?

  2. Alexandre Aquiles 18/08/2016 at 05:38 #

    Fala, Rafael!

    É bem por aí. Como não dá pra rodar sempre, joga pro CI server rodar periodicamente.

    Vale tratar os mutantes do PIT como as métricas do Sonar: gera de tempos em tempos (não precisa exatamente ser a todo commit) e dá um olhada. Só tem que tomar o cuidado de não ignorar…

  3. Gabriel Feitosa 18/08/2016 at 14:25 #

    Parabéns pelo post Alexandre!

  4. Jhonatan V P Normais 22/08/2016 at 08:38 #

    Obrigado pelas dicas.

  5. Raphael Lacerda 24/08/2016 at 13:52 #

    legal xande.. testando agora! não conhecia o PIT

  6. Raphael Lacerda 24/08/2016 at 14:23 #

    95.7% de cobertura. 4 mutators survived. 71 Killed!
    nice

  7. Jackson Antonio do Prado Lima 01/09/2016 at 08:01 #

    Alexandre, ótimo post!
    Seria interessante comentar sobre o teste de mutação de ordem superior, eles crescem exponecialmente em relação ao número de mutantes tradicionais. Se esses demoram imagina os de ordem superior!

    Outra ferramenta interessante é o Bacterio (http://alarcos.esi.uclm.es/per/preales/bacterio/bacterio.htm) que utiliza algoritmos search-based na busca por mutantes.

  8. Alexandre Aquiles 01/09/2016 at 08:27 #

    Muito interessante, Jackson. Valeu pela contribuição!

  9. Helcio da Silva 01/01/2017 at 20:47 #

    Parabéns pelo post Alexandre!

  10. Vinicius Dias 25/07/2017 at 17:38 #

    Muito legal. Tem sugestões de ferramentas pra outras linguagens? Não achei nada pra PHP. =/

  11. Alexandre Aquiles 25/07/2017 at 19:21 #

    Olá, Vinicius.

    Pelo que vi na Wikipédia [1], há uma ferramenta de teste de mutantes em PHP chamada Humbug [2]. Dê uma olhada!

    [1] https://en.wikipedia.org/wiki/Mutation_testing#External_links
    [2] https://github.com/humbug/humbug

Deixe uma resposta