O que eu faço
Tenho dois modos de operação que funcionam juntos:
-
DEDUP — Identificar e remover arquivos/cenários de teste redundantes onde outro arquivo já cobre os mesmos caminhos de código com os mesmos valores de entrada. Verificado através da comparação de cobertura gcov (antes vs. depois).
-
GAP-FILL — Após a deduplicação, analisar o relatório de cobertura para encontrar código não testado ou subtestado (cabeçalhos e fontes compilados), e então escrever testes unitários direcionados para preencher essas lacunas. Verificado através da melhoria de cobertura gcov (antes vs. depois).
Ambos os modos compartilham um fluxo de trabalho comum de construção e medição de cobertura.
Quando me usar
Para DEDUP:
- Sua suíte de testes C++ cresceu organicamente com vários arquivos cobrindo o mesmo terreno
- Arquivos de teste de preenchimento de lacunas (
*_gaps.cpp) foram adicionados junto com arquivos de teste abrangentes (*_comprehensive.cpp) - Você herdou uma base de código com cenários de teste duplicados
- Você quer reduzir o tempo de teste no CI sem perder cobertura
Para GAP-FILL:
- Você tem arquivos fonte
.cppcompilados sem um arquivo de teste correspondente - O relatório de cobertura mostra cabeçalhos com cobertura de linha <90%
- Você acabou de remover testes redundantes e quer reinvestir o tempo de teste economizado no preenchimento de lacunas
- Você precisa aumentar a cobertura antes de um prazo de lançamento
NÃO me use quando:
- Os testes são parametrizados/gerativos testando a mesma propriedade (essa é uma redundância intencional)
- O projeto não possui ferramentas de cobertura configuradas
- Você só precisa adicionar testes sem primeiro verificar redundância
Como eu trabalho
Fase 0: Entender o cenário de testes
Ler o diretório de teste e o CMakeLists.txt:
test/source/*.cpp
test/CMakeLists.txt
include/<project>/*.hpp
source/*.cpp # fontes compilados
Verificar:
- Pares de padrões:
*_gaps.cpp+*_comprehensive.cpp,*_basic.cpp+*_detailed.cpp - Arquivos
.cppcompilados emsource/semtest_*.cppcorrespondente - Cabeçalhos em
include/sem arquivo de teste dedicado - Framework de teste (doctest, Catch2, Google Test, etc.) e globbing do CMake
Observar:
- Framework de teste e macros usadas (
TEST_CASE,CHECK_EQ, etc.) - Se o CMakeLists.txt usa
file(GLOB ...)ou listas de fontes explícitas - O padrão de flags de cobertura (geralmente
-DENABLE_TEST_COVERAGE=ON)
Fase 1: Detectar redundância no nível de ARQUIVO
Para cada arquivo candidato, comparar seus cenários de teste contra todos os outros arquivos de teste na suíte.
Método de comparação concreto:
-
Ler ambos os arquivos lado a lado. Agrupar cenários de teste pelo código que eles exercitam:
- Funções livres (gcd, lcm, etc.)
- Métodos de classe (construtores, operadores, acessores, modificadores)
- Instanciações e especializações de template
- Cobertura de casos de borda (zero, infinito, NaN, valores limite)
-
Verificar duplicação no nível de valor. Se ambos os arquivos testam a mesma função com valores idênticos (ex.:
gcd_recur(12, 8) = 4em ambos), esse cenário é redundante. Variações menores (números de Fibonacci em um, mas não no outro) NÃO são redundantes — essas são entradas únicas. -
Marcar um arquivo como "redundante" se >90% de seus cenários de teste aparecem em outro arquivo com a mesma cobertura ou mais abrangente. Os <10% restantes de cenários únicos (variações triviais) podem ser descartados com segurança.
Fase 2: Construir linha de base (ANTES da remoção)
Construir a suíte de testes com instrumentação de cobertura:
| Ferramenta | Configuração |
|---|---|
| GCC + gcov | cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles" |
| Clang + llvm-cov | cmake -S test -B build/cov -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" -G "Ninja" |
| MSVC | Usar OpenCppCoverage ou a cobertura integrada do VS Enterprise |
Depois construir e executar:
cmake --build build/cov
./build/cov/<TestExecutable>
Gerar cobertura para cabeçalhos de bibliotecas
cd build/cov
# Para cada cabeçalho da biblioteca, executar gcov contra um arquivo objeto de teste que o #inclui
gcov -o CMakeFiles/<Tests>.dir/source/<test_file>.cpp.o <path>/include/<project>/<header>.hpp
# Para cada fonte compilado, executar gcov contra o arquivo objeto da biblioteca
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<source>.cpp.o <path>/source/<source>.cpp
Registrar métricas da linha de base:
Cabeçalhos:
array_like.hpp: 100% (28/28)
bpqueue.hpp: 100% (73/73)
...
Fontes compilados:
ell1d.cpp: 0% (0/23) ← NÃO TESTADO!
ldlt_mgr.cpp: 100% (10/10)
...
Casos de teste: N | Asserções: M | Status: SUCESSO
Também registrar as linhas exatas não cobertas para cada arquivo (as linhas #####: na saída .gcov).
Fase 3: Remover os arquivos redundantes (modo DEDUP)
Remove-Item -Force test/source/test_*_gaps.cpp # PowerShell
git status test/
git diff --stat
Fase 4: Reconstruir e coletar métricas APÓS DEDUP
# Reconfigurar (o GLOB do CMake pode detectar arquivos faltando)
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>
Comparar cobertura com a linha de base. Se a cobertura de qualquer cabeçalho cair ou as linhas não cobertas mudarem, reverter imediatamente — o arquivo NÃO era redundante.
Critérios de veredito para DEDUP:
| Verificação | Requisito |
|---|---|
| Todos os testes passam | Deve passar (antes e depois) |
| Cobertura de linha por cabeçalho | Exatamente igual antes e depois |
| Linhas não cobertas | Conjunto idêntico de linhas não cobertas |
| Contagem de testes diminuiu | Esperado — casos de teste removidos |
| Contagem de asserções diminuiu | Esperado — asserções removidas |
Fase 5: Analisar lacunas de cobertura (modo GAP-FILL)
Se após a deduplicação (ou começando do zero) o relatório de cobertura mostrar lacunas, identificar código não testado:
Tipo de lacuna A: Fonte compilado com cobertura zero
Verificar source/*.cpp contra test/source/*.cpp. Qualquer .cpp em source/ sem um test_*.cpp correspondente provavelmente não é testado.
# Encontrar arquivos fonte sem cobertura de teste
Get-ChildItem source/*.cpp | ForEach-Object {
$base = $_.Name -replace '\.cpp$'
$test = "test/source/test_$base.cpp"
if (-not (Test-Path $test)) { Write-Host "NÃO TESTADO: $($_.Name)" }
}
Tipo de lacuna B: Cabeçalhos de bibliotecas com baixa cobertura
Olhar o relatório gcov para cabeçalhos com cobertura de linha <90% ou "sem linhas executáveis". Cabeçalhos com muitos templates podem atribuir cobertura ao arquivo .cpp — verificar ambos.
Tipo de lacuna C: Funções/ramos dentro de arquivos cobertos
As linhas não cobertas (#####: no gcov) dentro de arquivos parcialmente cobertos revelam ramos específicos que não são exercitados. Esses são os alvos de maior valor.
Fase 6: Escrever testes direcionados
6a: Ler o código fonte não testado
Ler o arquivo fonte completamente para entender:
- A API da classe/função (construtores, métodos, parâmetros)
- Todos os ramos condicionais (if/else, casos switch, retornos antecipados)
- Casos de borda (entradas nulas/zero, valores limite, condições de erro)
- O comportamento matemático ou algorítmico
6b: Projetar casos de teste que cubram cada ramo
Para cada método, enumerar os cenários de teste necessários:
| Método | Condição | Entrada | Comportamento esperado |
|---|---|---|---|
update(cut) | beta > tau | g=1, beta=10 com _r=5 | Retorna NoSoln, estado inalterado |
update(cut) | beta < -tau | g=1, beta=-10 com _r=5 | Retorna NoEffect, estado inalterado |
update(cut) | ` | beta | <= tau, g>0` |
update(cut) | ` | beta | <= tau, g<0` |
update_central_cut(cut) | g > 0 | g=1, beta=0 | Divide _r pela metade, desloca centro para a esquerda |
update_central_cut(cut) | g < 0 | g=-1, beta=0 | Divide _r pela metade, desloca centro para a direita |
Incluir casos de borda:
- Intervalos de largura zero
- Valores limite (
beta == tau,beta == -tau) - Gradiente zero (se aplicável)
- Operações repetidas/encadeadas para verificar transições de estado
6c: Escrever o arquivo de teste
Escrever um novo test_<module>.cpp em test/source/ seguindo os padrões de teste existentes do projeto:
#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
#include <doctest/doctest.h>
#include <ellalgo/<module>.hpp> // cabeçalho sob teste
#include <ellalgo/ell_config.hpp> // se necessário para definições de tipo
#include <utility> // se necessário para std::pair
TEST_CASE("<Módulo>: construção") {
auto x = <Módulo>(...);
CHECK_EQ(x.some_method(), valor_esperado);
}
TEST_CASE("<Módulo>: método com caminho de sucesso") {
auto x = <Módulo>(...);
auto status = x.update(...);
CHECK_EQ(status, CutStatus::Success);
CHECK_EQ(x.state(), estado_esperado);
}
TEST_CASE("<Módulo>: método com caminho de falha") {
auto x = <Módulo>(...);
auto status = x.update(...); // dispara o ramo beta > tau
CHECK_EQ(status, CutStatus::NoSoln);
}
TEST_CASE("<Módulo>: casos de borda") {
// Intervalo de largura zero, condições limite, etc.
}
TEST_CASE("<Módulo>: operações encadeadas") {
// Múltiplas atualizações em sequência
}
Regras para valores de teste:
- Usar valores esperados hardcoded (não computados a partir do mesmo código sob teste)
- Anotar cada valor esperado com a derivação matemática em um comentário (ex.:
// tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5) - Testar cada ramo
ifpelo menos uma vez - Testar cada caso
switchpelo menos uma vez - Testar pelo menos uma operação encadeada/sequencial para verificar a persistência de estado
Fase 7: Reconstruir e coletar métricas APÓS GAP-FILL
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>
# Regenerar relatórios de cobertura
gcov -o CMakeFiles/<Tests>.dir/source/test_<module>.cpp.o <path>/include/<project>/<module>.hpp
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<module>.cpp.o <path>/source/<module>.cpp
Fase 8: Relatar melhoria de cobertura
| Métrica | Antes | Depois | Δ |
|--------|--------|-------|---|
| Casos de teste | N | N' | +Δ |
| Asserções | M | M' | +Δ |
| <module>.hpp | XX% | 100% | +Δ |
| <module>.cpp (compilado) | 0% | 100% | +100% |
| Todos os outros arquivos | ≥90% | ≥90% | 0% |
Lacunas de cobertura preenchidas: ✅
REGRAS CRÍTICAS
Regra 1: Cobertura é a verdade, não a contagem de testes
Um arquivo de teste é redundante apenas se removê-lo deixar a cobertura de linha de cada cabeçalho da biblioteca inalterada. Se a cobertura cair, o arquivo estava cobrindo caminhos de código únicos. Reverter imediatamente.
Ao adicionar testes, um novo teste só é bem-sucedido se aumentar a cobertura do arquivo alvo. Se a cobertura do arquivo alvo não aumentar, o teste não está exercitando o código — revisitar a lógica do teste.
Regra 2: Verificar linhas não cobertas, não apenas a porcentagem
Duas execuções podem mostrar "98.06%" mas ter linhas não cobertas diferentes. Sempre comparar o conjunto exato de linhas #####: na saída do arquivo .gcov:
Get-Content build/cov/foo.hpp.gcov | Select-String -Pattern "^ #####:"
Elas devem ser idênticas antes/depois da deduplicação. Para preenchimento de lacunas, as linhas #####: para o arquivo alvo devem diminuir.
Regra 3: NÃO deduplicar testes baseados em propriedades
Testes baseados em propriedades/gerativos (ex.: RapidCheck) são intencionalmente redundantes por design. Eles verificam invariantes com entradas aleatórias. Eles NÃO devem ser removidos, mesmo que se sobreponham a testes baseados em exemplos.
Regra 4: NÃO deduplicar testes com valores diferentes
Dois testes para o mesmo operador com valores de entrada diferentes exercitam caminhos de código diferentes. Remover apenas quando os valores de entrada e as saídas esperadas são idênticos.
Ao escrever novos testes, usar valores de entrada DIFERENTES dos testes existentes. Duplicar os mesmos valores não aumenta a cobertura.
Regra 5: Sempre reconstruir a partir de um estado limpo
Os arquivos .gcda são consumidos pelo gcov e ficam obsoletos após qualquer mudança no código fonte ou binário. Sempre reconstruir e reexecutar antes de medir:
cmake --build build/cov --clean-first # ou reconstruir do zero
./build/cov/<TestExecutable>
Regra 6: Verificar o comportamento GLOB do CMake
Se o CMakeLists.txt usa file(GLOB ...) com CONFIGURE_DEPENDS, o CMake detecta automaticamente arquivos adicionados/removidos na reconstrução (CMake 3.12+). Em CMake mais antigo, reconfigurar manualmente após adicionar ou remover arquivos de teste.
Ao escrever um novo arquivo de teste, verificar se o CMake o detecta:
- Procurar por
test_<module>.cppna saída da construção - Se estiver faltando, reconfigurar:
cmake -S test -B build/cov
Regra 7: Hardcodar valores esperados, não recalcular
Ao escrever testes, NUNCA calcular valores esperados usando o mesmo código sob teste. Sempre derivá-los manualmente e hardcodá-los como literais. Isso detecta regressões onde a implementação muda, mas o teste ainda passa porque ambos os lados usam o mesmo código quebrado.
Anotar derivações complexas:
// tau = |5*1| = 5, bound = 5 - 2/1 = 3
// g>0 so u=3, l=5-5=0
// _r = half_nonnegative(3-0) = 1.5, _xc = 0 + 1.5 = 1.5
CHECK_EQ(e.xc(), 1.5);
Regra 8: Testar cada ramo
Para cada função com lógica condicional, enumerar todos os ramos e escrever pelo menos um teste por ramo:
| Ramo | Cenário de teste |
|---|---|
if (beta > tau) | Caminho sem solução |
if (beta < -tau) | Caminho sem efeito |
else (sucesso) | Caminho de sucesso |
g > 0 dentro do sucesso | Gradiente positivo |
g < 0 dentro do sucesso | Gradiente negativo |
Limite beta == tau | Ainda sucesso (≤) |
Limite beta == -tau | Depende de < vs ≤ |
Regra 9: Priorizar fontes compilados sobre cabeçalhos
Arquivos .cpp compilados são mais propensos a ter cobertura zero porque o código de template somente cabeçalho é frequentemente exercitado incidentalmente por outros testes. Priorizar o teste de:
source/*.cppsemtest_*.cppcorrespondente → maior prioridade- Cabeçalhos com cobertura de linha
<90%→ prioridade média - Ramos não cobertos dentro de arquivos já cobertos → menor prioridade
Regra 10: Corresponder às convenções de teste do projeto
Antes de escrever testes, estudar 2-3 arquivos de teste existentes para corresponder:
- …
[truncado para tradução]