Lo que hago
Tengo dos modos de operación que trabajan juntos:
-
DEDUP — Identificar y eliminar archivos de prueba redundantes o escenarios donde otro archivo ya cubre las mismas rutas de código con los mismos valores de entrada. Verificado mediante comparación de cobertura gcov (antes vs. después).
-
GAP-FILL — Después de la deduplicación, analizar el informe de cobertura para encontrar código no probado o insuficientemente probado (en cabeceras y fuentes compiladas), luego escribir pruebas unitarias dirigidas para llenar esos vacíos. Verificado mediante la mejora de cobertura gcov (antes vs. después).
Ambos modos comparten un flujo de trabajo común de compilación de línea base y medición de cobertura.
Cuándo usarme
Para DEDUP:
- Tu conjunto de pruebas C++ ha crecido orgánicamente con múltiples archivos que cubren el mismo terreno.
- Se agregaron archivos de prueba para cubrir vacíos (
*_gaps.cpp) junto a archivos de prueba completos (*_comprehensive.cpp). - Heredaste una base de código con escenarios de prueba duplicados.
- Quieres reducir el tiempo de las pruebas en CI sin perder cobertura.
Para GAP-FILL:
- Tienes archivos fuente
.cppcompilados sin un archivo de prueba correspondiente. - El informe de cobertura muestra cabeceras con menos del 90% de cobertura de líneas.
- Acabas de eliminar pruebas redundantes y quieres reinvertir el tiempo de prueba ahorrado en llenar vacíos.
- Necesitas aumentar la cobertura antes de una fecha límite de lanzamiento.
NO me uses cuando:
- Las pruebas son parametrizadas/generativas que verifican la misma propiedad (esa redundancia es intencional).
- El proyecto no tiene herramientas de cobertura configuradas.
- Solo necesitas agregar pruebas sin verificar primero la redundancia.
Cómo trabajo
Fase 0: Entender el panorama de pruebas
Leer el directorio de pruebas y el CMakeLists.txt:
test/source/*.cpp
test/CMakeLists.txt
include/<project>/*.hpp
source/*.cpp # fuentes compiladas
Verificar:
- Pares de patrones:
*_gaps.cpp+*_comprehensive.cpp,*_basic.cpp+*_detailed.cpp - Archivos
.cppcompilados ensource/que no tienen untest_*.cppcorrespondiente. - Cabeceras en
include/sin un archivo de prueba dedicado. - Framework de pruebas (doctest, Catch2, Google Test, etc.) y cómo se hace el globbing en CMake.
Nota:
- Framework de pruebas y macros utilizados (
TEST_CASE,CHECK_EQ, etc.). - Si CMakeLists.txt usa
file(GLOB ...)o listas de fuentes explícitas. - El patrón de banderas de cobertura (normalmente
-DENABLE_TEST_COVERAGE=ON).
Fase 1: Detectar redundancia a nivel de ARCHIVO
Para cada archivo candidato, comparar sus escenarios de prueba contra cada otro archivo de prueba en el conjunto.
Método de comparación concreto:
-
Leer ambos archivos lado a lado. Agrupar los escenarios de prueba según el código que ejercitan:
- Funciones libres (mcd, mcm, etc.)
- Métodos de clase (constructores, operadores, accesores, mutadores)
- Instanciaciones y especializaciones de plantillas
- Cobertura de casos límite (cero, infinito, NaN, valores frontera)
-
Verificar duplicación a nivel de valor. Si ambos archivos prueban la misma función con valores idénticos (por ejemplo,
mcd_recur(12, 8) = 4en ambos), ese escenario es redundante. Variaciones menores (números de Fibonacci en uno pero no en el otro) NO son redundantes — esas son entradas únicas. -
Marcar un archivo como "redundante" si >90% de sus escenarios de prueba aparecen en otro archivo con la misma cobertura o una más completa. El <10% restante de escenarios únicos (variaciones triviales) se puede descartar de manera segura.
Fase 2: Compilar la línea base (ANTES de la eliminación)
Compilar el conjunto de pruebas con instrumentación de cobertura:
| Herramienta | Configuración |
|---|---|
| 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 | Usa OpenCppCoverage o la cobertura integrada de VS Enterprise |
Luego compilar y ejecutar:
cmake --build build/cov
./build/cov/<TestExecutable>
Generar cobertura para cabeceras de bibliotecas
cd build/cov
# Para cada cabecera de biblioteca, ejecutar gcov contra un archivo objeto de prueba que la #incluye
gcov -o CMakeFiles/<Tests>.dir/source/<archivo_prueba>.cpp.o <ruta>/include/<proyecto>/<cabecera>.hpp
# Para cada fuente compilada, ejecutar gcov contra el archivo objeto de la biblioteca
gcov -o _deps/<proyecto>-build/CMakeFiles/<Project>.dir/source/<fuente>.cpp.o <ruta>/source/<fuente>.cpp
Registrar métricas de línea base:
Cabeceras:
array_like.hpp: 100% (28/28)
bpqueue.hpp: 100% (73/73)
...
Fuentes compiladas:
ell1d.cpp: 0% (0/23) ← ¡NO PROBADO!
ldlt_mgr.cpp: 100% (10/10)
...
Casos de prueba: N | Aserciones: M | Estado: ÉXITO
También registrar las líneas exactas no cubiertas para cada archivo (las líneas #####: en la salida de .gcov).
Fase 3: Eliminar los archivos redundantes (modo DEDUP)
Remove-Item -Force test/source/test_*_gaps.cpp # PowerShell
git status test/
git diff --stat
Fase 4: Recompilar y recopilar métricas DESPUÉS DE LA DEDUPLICACIÓN
# Reconfigurar (CMake GLOB puede detectar archivos faltantes)
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 la cobertura contra la línea base. Si la cobertura de alguna cabecera disminuye o las líneas no cubiertas cambian, revertir inmediatamente — el archivo NO era redundante.
Criterios de veredicto para DEDUP:
| Verificación | Requisito |
|---|---|
| Todas las pruebas pasan | Debe pasar (tanto antes como después) |
| Cobertura de líneas por cabecera | Exactamente igual antes y después |
| Líneas no cubiertas | Conjunto idéntico de líneas no cubiertas |
| Número de pruebas disminuyó | Esperado — se eliminaron casos de prueba |
| Número de aserciones disminuyó | Esperado — se eliminaron aserciones |
Fase 5: Analizar las brechas de cobertura (modo GAP-FILL)
Si después de la deduplicación (o empezando desde cero) el informe de cobertura muestra brechas, identificar el código no probado:
Brecha Tipo A: Fuente compilada con cobertura cero
Verificar source/*.cpp contra test/source/*.cpp. Cualquier .cpp en source/ sin un test_*.cpp correspondiente probablemente no está probado.
# Encontrar archivos fuente sin cobertura de prueba
Get-ChildItem source/*.cpp | ForEach-Object {
$base = $_.Name -replace '\.cpp$'
$test = "test/source/test_$base.cpp"
if (-not (Test-Path $test)) { Write-Host "NO PROBADO: $($_.Name)" }
}
Brecha Tipo B: Cabeceras de bibliotecas con baja cobertura
Mirar el informe de gcov para cabeceras con <90% de cobertura de líneas o "sin líneas ejecutables". Las cabeceras con muchas plantillas pueden atribuir la cobertura al archivo .cpp — verificar ambos.
Brecha Tipo C: Funciones/ramas dentro de archivos cubiertos
Las líneas no cubiertas (#####: en gcov) dentro de archivos parcialmente cubiertos revelan ramas específicas que no se ejercitan. Estos son los objetivos de mayor valor.
Fase 6: Escribir pruebas dirigidas
6a: Leer el código fuente no probado
Leer el archivo fuente completo para entender:
- La API de la clase/función (constructores, métodos, parámetros)
- Todas las ramas condicionales (if/else, casos switch, retornos tempranos)
- Casos límite (entradas nulas/cero, valores frontera, condiciones de error)
- El comportamiento matemático o algorítmico
6b: Diseñar casos de prueba que cubran cada rama
Para cada método, enumerar los escenarios de prueba necesarios:
| Método | Condición | Entrada | Comportamiento esperado |
|---|---|---|---|
update(cut) | beta > tau | g=1, beta=10 con _r=5 | Devuelve NoSoln, estado sin cambios |
update(cut) | beta < -tau | g=1, beta=-10 con _r=5 | Devuelve NoEffect, estado sin cambios |
update(cut) | |beta| <= tau, g>0 | g=1, beta=2 con _r=5 | Devuelve Success, nuevo centro en l+_r |
update(cut) | |beta| <= tau, g<0 | g=-1, beta=2 con _r=5 | Devuelve Success, nuevo centro en l+_r |
update_central_cut(cut) | g > 0 | g=1, beta=0 | Reduce _r a la mitad, desplaza el centro a la izquierda |
update_central_cut(cut) | g < 0 | g=-1, beta=0 | Reduce _r a la mitad, desplaza el centro a la derecha |
Incluir casos límite:
- Intervalos de ancho cero
- Valores frontera (
beta == tau,beta == -tau) - Gradiente cero (si aplica)
- Operaciones repetidas/encadenadas para verificar transiciones de estado
6c: Escribir el archivo de prueba
Escribir un nuevo test_<módulo>.cpp en test/source/ siguiendo los patrones de prueba existentes del proyecto:
#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
#include <doctest/doctest.h>
#include <ellalgo/<módulo>.hpp> // cabecera bajo prueba
#include <ellalgo/ell_config.hpp> // si es necesario para definiciones de tipos
#include <utility> // si es necesario para std::pair
TEST_CASE("<Módulo>: construcción") {
auto x = <Módulo>(...);
CHECK_EQ(x.método_(), valor_esperado);
}
TEST_CASE("<Módulo>: método con ruta de éxito") {
auto x = <Módulo>(...);
auto estado = x.update(...);
CHECK_EQ(estado, CutStatus::Success);
CHECK_EQ(x.estado(), estado_esperado);
}
TEST_CASE("<Módulo>: método con ruta de fallo") {
auto x = <Módulo>(...);
auto estado = x.update(...); // dispara la rama beta > tau
CHECK_EQ(estado, CutStatus::NoSoln);
}
TEST_CASE("<Módulo>: casos límite") {
// Intervalo de ancho cero, condiciones frontera, etc.
}
TEST_CASE("<Módulo>: operaciones encadenadas") {
// Varias actualizaciones en secuencia
}
Reglas para los valores de prueba:
- Usar valores esperados codificados (no calculados a partir del mismo código bajo prueba).
- Anotar cada valor esperado con la derivación matemática en un comentario (por ejemplo,
// tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5). - Probar cada rama
ifal menos una vez. - Probar cada caso
switchal menos una vez. - Probar al menos una operación encadenada/secuencial para verificar la persistencia del estado.
Fase 7: Recompilar y recopilar métricas DESPUÉS DE LLENAR LAS BRECHAS
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 informes de cobertura
gcov -o CMakeFiles/<Tests>.dir/source/test_<módulo>.cpp.o <ruta>/include/<proyecto>/<módulo>.hpp
gcov -o _deps/<proyecto>-build/CMakeFiles/<Project>.dir/source/<módulo>.cpp.o <ruta>/source/<módulo>.cpp
Fase 8: Reportar la mejora de cobertura
| Métrica | Antes | Después | Δ |
|--------|--------|-------|---|
| Casos de prueba | N | N' | +Δ |
| Aserciones | M | M' | +Δ |
| <módulo>.hpp | XX% | 100% | +Δ |
| <módulo>.cpp (compilado) | 0% | 100% | +100% |
| Todos los demás archivos | ≥90% | ≥90% | 0% |
Brechas de cobertura llenadas: ✅
REGLAS CRÍTICAS
Regla 1: La cobertura es la verdad, no el número de pruebas
Un archivo de prueba es redundante solo si su eliminación deja sin cambios la cobertura de líneas de cada cabecera de biblioteca. Si la cobertura disminuye, el archivo estaba cubriendo rutas de código únicas. Revertir inmediatamente.
Al agregar pruebas, una nueva prueba solo es exitosa si aumenta la cobertura del archivo objetivo. Si la cobertura del archivo objetivo no aumenta, la prueba no está ejercitando el código — revisar la lógica de la prueba.
Regla 2: Verificar las líneas no cubiertas, no solo el porcentaje
Dos ejecuciones pueden mostrar "98.06%" pero tener diferentes líneas no cubiertas. Siempre comparar el conjunto exacto de líneas #####: en la salida del archivo .gcov:
Get-Content build/cov/foo.hpp.gcov | Select-String -Pattern "^ #####:"
Estas deben ser idénticas antes/después de la deduplicación. Para el llenado de brechas, las líneas #####: del archivo objetivo deben disminuir.
Regla 3: NO eliminar pruebas basadas en propiedades
Las pruebas basadas en propiedades / generativas (por ejemplo, RapidCheck) son intencionalmente redundantes por diseño. Verifican invariantes con entradas aleatorizadas. Estas NO deben eliminarse incluso si se superponen con pruebas basadas en ejemplos.
Regla 4: NO eliminar pruebas con diferentes valores
Dos pruebas para el mismo operador con diferentes valores de entrada ejercitan diferentes rutas de código. Solo eliminar cuando los valores de entrada y las salidas esperadas son idénticos.
Al escribir nuevas pruebas, usar valores de entrada DIFERENTES a los de las pruebas existentes. Duplicar los mismos valores no aumenta la cobertura.
Regla 5: Siempre reconstruir desde un estado limpio
Los archivos .gcda son consumidos por gcov y se vuelven obsoletos después de cualquier cambio en el código fuente o binario. Siempre reconstruir y volver a ejecutar antes de medir:
cmake --build build/cov --clean-first # o reconstruir desde cero
./build/cov/<TestExecutable>
Regla 6: Verificar el comportamiento de GLOB de CMake
Si CMakeLists.txt usa file(GLOB ...) con CONFIGURE_DEPENDS, CMake detecta automáticamente archivos agregados/eliminados al reconstruir (CMake 3.12+). En CMake más antiguo, reconfigurar manualmente después de agregar o eliminar archivos de prueba.
Al escribir un nuevo archivo de prueba, verificar que CMake lo reconozca:
- Buscar
test_<módulo>.cppen la salida de compilación. - Si falta, reconfigurar:
cmake -S test -B build/cov
Regla 7: Codificar los valores esperados, no recalcularlos
Al escribir pruebas, NUNCA calcular los valores esperados usando el mismo código bajo prueba. Siempre derivarlos manualmente y codificarlos como literales. Esto detecta regresiones donde la implementación cambia pero la prueba sigue pasando porque ambos lados usan el mismo código defectuoso.
Anotar derivaciones complejas:
// tau = |5*1| = 5, bound = 5 - 2/1 = 3
// g>0 entonces 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);
Regla 8: Probar cada rama
Para cada función con lógica condicional, enumerar todas las ramas y escribir al menos una prueba por rama:
| Rama | Escenario de prueba |
|---|---|
if (beta > tau) | Ruta sin solución |
if (beta < -tau) | Ruta sin efecto |
else (éxito) | Ruta de éxito |
g > 0 dentro del éxito | Gradiente positivo |
g < 0 dentro del éxito | Gradiente negativo |
Frontera beta == tau | Aún éxito (≤) |
Frontera beta == -tau | Depende de < vs ≤ |
Regla 9: Priorizar fuentes compiladas sobre cabeceras
Es más probable que los archivos .cpp compilados tengan cobertura cero porque el código de solo cabeceras a menudo se ejercita incidentalmente por otras pruebas. Priorizar la prueba de:
source/*.cppsintest_*.cppcorrespondiente → prioridad más alta- Cabeceras con cobertura de líneas <90% → prioridad media
- Ramas no cubiertas dentro de archivos ya cubiertos → prioridad más baja
Regla 10: Coincidir con las convenciones de prueba del proyecto
Antes de escribir pruebas, estudiar 2-3 archivos de prueba existentes para coincidir con:
...
[truncado para la traducción]