Testes e qualidade de software
Qualidade de código e análise estática: Error Prone, SpotBugs, PMD e SonarQube
Intermediário11 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
A análise estática lê o código sem executá-lo e encontra uma classe de defeito que nenhuma suíte de testes vai alcançar: o branch inalcançável, o recurso que nunca é fechado, o equals que compara um array. Este guia separa as famílias de ferramentas, mostra como configurar um quality gate que os desenvolvedores respeitem e descreve como adotar a análise em uma base de código com dez mil apontamentos já existentes.
O que você vai aprender
- Explicar o que a análise estática detecta e que os testes dinâmicos, por natureza, não conseguem detectar
- Diferenciar formatadores, verificadores de estilo, detectores de bugs, SAST e scanners de dependências
- Configurar Error Prone, SpotBugs, PMD e Checkstyle para que valha a pena ler o que eles apontam
- Definir um quality gate do SonarQube sobre o código novo, e não sobre a base de código inteira
- Interpretar métricas de complexidade, duplicação e dívida técnica sem confiar demais nelas
- Introduzir a análise em uma base de código legada sem gerar um backlog inutilizável
Os testes dizem se o código fez a coisa certa com as entradas que você escolheu. A análise estática lê o código sem executá-lo e raciocina sobre todos os caminhos, inclusive os que nenhum teste jamais percorreu. Essa diferença é todo o argumento para usar os dois: uma suíte de testes com excelente cobertura ainda pode deixar passar um stream que nunca é fechado no caminho de exceção, um equals chamado sobre dois arrays, uma string de formato com o número errado de placeholders ou uma comparação de inteiros encapsulados com == que funciona até 127 e falha em 128.
O outro motivo para rodar análise é econômico. Um apontamento gerado por uma ferramenta não custa nada para ser detectado, chega segundos depois de o código ser escrito e não depende de ninguém concordar que ele merece um comentário no code review. O tempo de revisão humana é escasso e deve ser gasto em saber se o design está certo, não em saber se um recurso vaza, que é o mesmo argumento que o guia de padrões de código usa para automatizar a formatação.
O que a análise enxerga e o que ela não consegue enxergar
Os analisadores trabalham sobre a árvore sintática abstrata, o grafo de fluxo de controle e, nas ferramentas mais fortes, uma aproximação do fluxo de dados. Isso lhes dá capacidades que nenhum teste tem: eles podem considerar um branch que nunca foi executado, provar que um null pode chegar a uma desreferência independentemente da entrada ou rastrear dados de usuário contaminados desde um parâmetro da requisição até uma string SQL.
Isso também lhes impõe limites permanentes. Eles não sabem nada sobre a intenção, então nenhum analisador jamais vai dizer que uma alíquota de imposto está errada. Eles precisam aproximar, porque as perguntas de fundo são indecidíveis, e aproximar significa ou falsos positivos (apontar algo que é seguro) ou falsos negativos (deixar passar algo real). Cada ferramenta escolhe um ponto nesse espectro, e saber onde ela fica diz como tratar o que ela produz: o Error Prone é calibrado para ter quase zero falsos positivos e, por isso, pode quebrar um build, enquanto um scanner de segurança tende deliberadamente a apontar e exige triagem.
As famílias de ferramentas
Jogar tudo sob o rótulo de "análise estática" faz os times adotarem ferramentas que se sobrepõem e se afogarem nelas. São cinco papéis distintos:
| Família | Pergunta que responde | Exemplos | Comportamento no build |
|---|---|---|---|
| Formatador | O layout é o canônico? | Spotless, google-java-format | Reescreve e depois verifica |
| Verificador de estilo | Segue nossas convenções? | Checkstyle | Falha em violações |
| Detector de bugs | Este código provavelmente está errado? | Error Prone, SpotBugs, PMD | Falha em apontamentos de alta confiança |
| SAST | Isso é explorável? | Regras de segurança do SonarQube, Semgrep | Reporta, com triagem |
| Scanner de dependências | Nossas bibliotecas têm CVEs conhecidas? | OWASP Dependency-Check, Trivy | Falha em novas de severidade alta |
Adote uma de cada linha, não três da mesma. Dois verificadores de estilo com opiniões diferentes produzem apontamentos contraditórios e uma discussão.
Comece pelo compilador
Antes de qualquer ferramenta, o JDK já reporta muita coisa que a maioria dos projetos ignora. Ligar os warnings e tratá-los como erros é a melhoria de qualidade mais barata que existe:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
<compilerArgs>
<arg>-Xlint:all,-serial,-processing</arg>
<arg>-Werror</arg>
</compilerArgs>
</configuration>
</plugin>
-Xlint:all cobre operações genéricas não verificadas, depreciação, fall-through em switches, @Override ausente e mais. -Werror faz com que quebrem o build, que é a única configuração que funciona: um build que imprime quatrocentos warnings é um build em que ninguém lê warnings. Em um projeto existente, ative as categorias uma de cada vez.
Detectores de bugs que valem a pena
O Error Prone se conecta ao javac e analisa com toda a informação de tipos durante a compilação. Suas regras são selecionadas pela confiabilidade, e é por isso que muitas são erros por padrão: chamar Object.equals com tipos incompatíveis, um argumento incompatível com um @FormatMethod, um valor de retorno ignorado quando não pode ser, uma Duration comparada com ==. Como roda dentro do compilador, é rápido e pode oferecer correções automáticas.
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
<arg>-Xplugin:ErrorProne -Xep:MissingOverride:ERROR -Xep:UnusedVariable:WARN</arg>
</compilerArgs>
O SpotBugs analisa o bytecode compilado e encontra outro conjunto de problemas: desreferência de null em um caminho, vazamento de recursos, estado interno mutável exposto por um getter, sincronização inconsistente, classes Serializable com campos não serializáveis. O plugin find-sec-bugs acrescenta padrões de injeção e de criptografia. A análise de bytecode enxerga o que o compilador produziu, e não o que você escreveu, o que às vezes torna um apontamento surpreendente até você olhar o código gerado.
O PMD trabalha sobre o código-fonte e se especializa em regras de design e complexidade (métodos longos, parâmetros demais, condicionais muito aninhadas, blocos catch vazios), além do seu detector de copiar e colar, o CPD, que encontra blocos duplicados entre arquivos que nenhuma outra ferramenta desta lista percebe.
O Checkstyle impõe convenções, não correção: nomenclatura, ordem dos imports, Javadoc na API pública, estrutura dos arquivos. É o que menos se sobrepõe aos outros e o que mais precisa ter seu conjunto de regras enxugado para se ajustar ao time, porque uma configuração padrão vai reclamar de muita coisa com que ninguém decidiu se importar.
Duas regras de configuração valem para todas. Faça o build falhar nas regras que vocês decidiram que importam, porque um relatório que ninguém abre é um relatório que ninguém lê. E remova da configuração compartilhada as regras com que vocês não concordam, em vez de suprimi-las uma a uma no código: um comentário de supressão é uma decisão de um arquivo só que, silenciosamente, vira política.
SonarQube e o gate sobre o código novo
O SonarQube agrega seus próprios analisadores aos relatórios importados das ferramentas acima, guarda o histórico e aplica um quality gate: um conjunto de condições em que um build passa ou falha.
A decisão de configuração mais importante é que o gate se aplique ao código novo (as linhas adicionadas ou alteradas desde uma baseline escolhida), e não ao projeto inteiro. É isso que torna a análise adotável em uma base de código real. Um gate que exige 80% de cobertura em um sistema de quinze anos é inalcançável e vai ser desativado em um mês; um gate que exige 80% de cobertura nas linhas em que você mexeu nesta semana é um pedido razoável, e a base de código melhora onde quer que o trabalho esteja acontecendo.
Um gate viável sobre o código novo:
- Cobertura das linhas novas acima de 80%
- Linhas duplicadas no código novo abaixo de 3%
- Nenhuma issue nova classificada como blocker ou critical
- Nenhum security hotspot novo sem revisão
- Rating geral do código novo em A
O modelo mais recente do SonarQube classifica as issues conforme ameacem um código consistent, intentional, adaptable ou responsible, e as associa a qualidades de software (segurança, confiabilidade, manutenibilidade) em vez da antiga tríade bug/vulnerabilidade/code smell. O mapeamento importa menos do que a disciplina de fazer a triagem de cada blocker novo antes do merge.
Integre-o ao pipeline para que o gate seja uma condição de merge, e não um dashboard que alguém visita de vez em quando:
mvn -B verify sonar:sonar \
-Dsonar.projectKey=shop-service \
-Dsonar.qualitygate.wait=true
sonar.qualitygate.wait=true faz o comando bloquear até o resultado do gate ser conhecido e sair com código diferente de zero em caso de falha, e é isso que o transforma em um gate de verdade. O guia de integração contínua mostra onde isso entra no pipeline em relação às etapas de teste.
Métricas, avaliadas com honestidade
A análise estática produz números, e números vão parar em dashboards, signifiquem algo ou não.
A complexidade ciclomática conta os caminhos independentes em um método. É uma ferramenta de triagem razoável (ordene os métodos por ela e os piores da base de código costumam estar no topo) e uma meta ruim. Um switch plano sobre vinte valores de enum tem pontuação alta e é perfeitamente legível; um método com três loops aninhados e um continue tem pontuação menor e é muito mais difícil de acompanhar. A complexidade cognitiva foi criada justamente para resolver isso, penalizando o aninhamento e ignorando estruturas abreviadas, e acompanha bem melhor a dificuldade percebida por uma pessoa.
O percentual de duplicação é útil quando aponta lógica de negócio copiada e colada, que é onde um bug acaba corrigido em dois de três lugares. É ruído quando sinaliza código gerado, testes deliberadamente explícitos ou boilerplate de DTO.
A dívida técnica expressa como tempo de remediação ("4 dias 2 horas") é uma soma grosseira de estimativas por regra. Trate-a como um critério de ordenação, não como uma estimativa; ninguém nunca consertou uma base de código no tempo que a ferramenta previu. A tendência dela diz mais do que o valor, e o valor por módulo diz mais do que o total.
A cobertura é importada, e não calculada pelo analisador, e carrega a ressalva do guia de estratégia de automação de testes: ela mede execução, não asserção, e se comporta mal como meta.
Os ratings de manutenibilidade, confiabilidade e segurança comprimem contagens de issues em uma letra. Eles comunicam bem para quem está fora do time e escondem tudo de que um especialista precisa, então use-os para reportar e a lista de issues para o trabalho.
Análise focada em segurança
Duas varreduras distintas devem estar em todo pipeline.
O SAST procura padrões exploráveis no seu próprio código: SQL montado por concatenação de strings, um comando executado a partir da entrada do usuário, path traversal, hashing fraco, uma credencial hard-coded, uma verificação de certificado desativada, parsing de XML com entidades externas habilitadas. Os motores mais fortes fazem taint tracking (seguem os dados desde uma fonte não confiável até um ponto de uso perigoso), e é por isso que conseguem distinguir um SQL concatenado com uma constante de um SQL concatenado com um parâmetro da requisição.
O SCA (varredura de dependências) confronta suas bibliotecas com bases de vulnerabilidades. Na prática, ele encontra mais risco real do que o SAST, porque a maioria das aplicações entrega muito mais código de terceiros do que código próprio. As regras operacionais que o tornam suportável: barrar apenas apontamentos novos de severidade alta, para que a fila não bloqueie todo build; acompanhar as dependências transitivas além das diretas; e registrar os riscos aceitos com data de expiração, em vez de uma supressão permanente.
A varredura de segredos é uma terceira, barata e que vale a pena: uma varredura do diff em busca de chaves privadas, tokens e connection strings. Ela precisa rodar em um hook pre-receive ou em uma etapa inicial do pipeline, porque, depois que um segredo chega ao histórico compartilhado, o único remédio é rotacionar a credencial.
Nenhuma delas substitui uma revisão de segurança ou um teste de invasão, e uma varredura limpa é um piso, não uma garantia. O que elas fazem com confiabilidade é impedir que os padrões conhecidos cheguem à main.
Adotando a análise em uma base de código legada
A primeira execução completa em uma base de código estabelecida reporta milhares de apontamentos. Esse número é o motivo pelo qual a maioria das tentativas de adoção fracassa: um backlog desse tamanho não é acionável, então é ignorado, e, uma vez ignorado, a ferramenta vira ruído.
Uma sequência que funciona:
Congele a baseline
Rode a análise, registre o estado atual e configure o gate para se aplicar só ao código novo. O objetivo imediato é que o número pare de crescer, não que diminua.Ative em todo lugar um punhado de regras certeiras
Escolha de dez a quinze regras de correção de alta confiança (vazamento de recursos, valores de retorno ignorados, erros de comparação, blocos catch vazios) e faça delas falhas de build em toda a base de código. Corrija as violações existentes apenas dessas. Costuma dar um ou dois dias de trabalho e elimina defeitos reais.Automatize a formatação em um único commit
Aplique o formatador em toda a base de código em um único commit só de formatação, adicione o hash dele ao arquivo ignore-revs para que o histórico continue legível e passe a exigi-lo dali em diante. Isso elimina de vez uma categoria inteira de apontamentos.Corrija oportunisticamente
Quem altera um arquivo corrige os apontamentos na parte em que mexeu. Ao longo de um ano, os arquivos que importam, os que estão em mudança ativa, ficam limpos, e os dormentes continuam como estão, que é a distribuição correta de esforço.Adicione regras aos poucos
Uma nova categoria de regras por iteração, discutida com antecedência e acordada pelo time. Uma regra imposta sem discussão acaba suprimida em vez de seguida.Revise as supressões
Uma vez por trimestre, liste todas as supressões e exclusões. Algumas registram uma decisão que vale manter; outras foram um contorno para um falso positivo de uma versão específica que já foi corrigido.
O princípio de fundo é que a análise precisa fazer parte do ciclo em que o desenvolvedor já está. Um apontamento que aparece na IDE enquanto você digita custa segundos; o mesmo apontamento em um relatório noturno custa uma troca de contexto, e em uma auditoria trimestral custa uma reunião. Configure os plugins da IDE a partir dos mesmos arquivos de configuração compartilhados que o build usa, para que os resultados locais e os do pipeline batam: divergência entre os dois é o jeito mais rápido de fazer as pessoas desconfiarem de tudo.
Principais conclusões
- A análise estática raciocina sobre caminhos que nenhum teste exercita, e os testes verificam uma intenção que nenhum analisador pode conhecer; os dois cobrem classes de defeito diferentes e nenhum substitui o outro.
- Escolha uma ferramenta por família (formatador, verificador de estilo, detector de bugs, SAST, scanner de dependências) e configure o conjunto de regras compartilhado de forma deliberada, em vez de aceitar os padrões.
- Comece pelo compilador:
-Xlint:all -Werrornão custa nada e pega uma quantidade surpreendente de problemas. - Aplique o gate ao código novo, não à base de código inteira; essa escolha, sozinha, é o que torna a análise adotável em um sistema legado.
- Trate os números de complexidade, duplicação e dívida técnica como ferramentas de triagem e tendências, nunca como metas, e prefira a complexidade cognitiva para questões de legibilidade.
- A varredura de dependências costuma encontrar mais risco real do que a análise do seu próprio código, então barre apontamentos novos de severidade alta e dê data de expiração aos riscos aceitos.
Fontes e leituras complementares
Perguntas frequentes
- A análise estática substitui os testes?
- Não, e as duas encontram coisas diferentes. A análise raciocina sobre todos os caminhos do código, mas não sabe nada da intenção: ela consegue provar que um stream nunca é fechado e, ao mesmo tempo, ser totalmente cega a um desconto calculado com o percentual errado. Os testes verificam a intenção nos caminhos que exercitam. Um time precisa das duas coisas; tratar qualquer uma como substituta da outra deixa uma categoria inteira de defeitos descoberta.
- Como lidar com falsos positivos sem desativar a regra?
- Suprima no escopo mais restrito possível e registre o motivo: uma anotação naquele método específico, não uma exclusão para a classe ou o projeto inteiro, para que o próximo leitor possa avaliar se ela ainda se aplica. Se uma regra gera falsos positivos repetidamente em toda a base de código, é sinal de que ela deve ser desativada de propósito na configuração compartilhada, em vez de espalhar supressões. As duas decisões devem ficar visíveis no controle de versão.
- Complexidade ciclomática alta é sempre ruim?
- Nem sempre. Um método que faz um switch sobre vinte constantes de enum, com uma linha cada, tem complexidade alta e é perfeitamente legível, enquanto um método com complexidade seis e quatro níveis de aninhamento pode ser muito pior. Complexidade é uma ferramenta de triagem: ordene por ela, olhe o topo da lista e use o bom senso. A complexidade cognitiva, que pesa mais o aninhamento e ignora switches planos, reflete melhor o que as pessoas acham difícil de ler.
- Temos 12.000 apontamentos existentes. Por onde começar?
- Não comece corrigindo. Aplique o gate só ao código novo, para que o número pare de crescer, e aceite os apontamentos existentes como uma baseline conhecida. Depois, ative um pequeno conjunto de regras de correção de alta confiança como falha de build em toda a base de código e corrija só essas. Todo o resto é corrigido oportunisticamente, quando alguém mexe no arquivo. Tentar zerar o backlog primeiro é assim que as iniciativas de adoção morrem.