⚡ Flaky Test Patterns - Antipatrones y Soluciones¶
🎯 Qué es un Flaky Test¶
╔═════════════════════════════════════════════════════════════════════╗
║ ║
║ DEFINICIÓN ║
║ ║
║ Un flaky test es uno que exhibe comportamiento no-determinístico: ║
║ Pasa unas veces, falla otras, SIN cambios en el código. ║
║ ║
║ ┌─────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ Run 1: ✅ PASS │ ║
║ │ Run 2: ❌ FAIL │ ║
║ │ Run 3: ✅ PASS │ ║
║ │ Run 4: ✅ PASS │ ║
║ │ Run 5: ❌ FAIL │ ║
║ │ │ ║
║ │ → FLAKY │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────────────┘ ║
║ ║
║ 🚨 POR QUÉ SON PELIGROSOS: ║
║ • Destruyen confianza en el test suite ║
║ • Developers ignoran failures ("probably flaky") ║
║ • Ocultan bugs reales ║
║ • Waste tiempo investigando false positives ║
║ ║
║ 💡 REGLA DE ORO: ║
║ ZERO TOLERANCE para flaky tests. Fix inmediatamente o disable. ║
║ ║
╚═════════════════════════════════════════════════════════════════════╝
🎲 PATRÓN 1: Race Conditions¶
╔═════════════════════════════════════════════════════════════════════╗
║ ANTIPATRÓN: Multithreading sin sincronización ║
╚═════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ❌ BAD EXAMPLE │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("audio_processor_multithread") { │ │
│ │ AudioProcessor processor; │ │
│ │ AudioBuffer buffer; │ │
│ │ │ │
│ │ // Thread 1: Write │ │
│ │ std::thread writer([&]() { │ │
│ │ processor.process(buffer); │ │
│ │ }); │ │
│ │ │ │
│ │ // Thread 2: Read │ │
│ │ std::thread reader([&]() { │ │
│ │ float value = buffer.getSample(0); // ⚠️ RACE! │ │
│ │ REQUIRE(value == 0.5f); │ │
│ │ }); │ │
│ │ │ │
│ │ writer.join(); │ │
│ │ reader.join(); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ PROBLEMA: │
│ • Reader puede ejecutar antes que writer │
│ • Data race en buffer access │
│ • No hay garantía de orden de ejecución │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ✅ GOOD SOLUTION 1: Usar Mocks (Recomendado) │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("audio_processor_logic") { │ │
│ │ // NO threading en unit test │ │
│ │ AudioProcessor processor; │ │
│ │ AudioBuffer buffer = createTestBuffer(); │ │
│ │ │ │
│ │ processor.process(buffer); │ │
│ │ │ │
│ │ REQUIRE(buffer.getSample(0) == 0.5f); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ VENTAJAS: │
│ • Determinístico 100% │
│ • Rápido (no overhead de threading) │
│ • Fácil de debuggear │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ✅ GOOD SOLUTION 2: Sincronización Explícita │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("audio_processor_multithread_safe") { │ │
│ │ AudioProcessor processor; │ │
│ │ AudioBuffer buffer; │ │
│ │ std::mutex mtx; │ │
│ │ std::condition_variable cv; │ │
│ │ bool writerDone = false; │ │
│ │ │ │
│ │ std::thread writer([&]() { │ │
│ │ processor.process(buffer); │ │
│ │ { │ │
│ │ std::lock_guard<std::mutex> lock(mtx); │ │
│ │ writerDone = true; │ │
│ │ } │ │
│ │ cv.notify_one(); │ │
│ │ }); │ │
│ │ │ │
│ │ std::thread reader([&]() { │ │
│ │ // Wait for writer │ │
│ │ std::unique_lock<std::mutex> lock(mtx); │ │
│ │ cv.wait(lock, [&]{ return writerDone; }); │ │
│ │ │ │
│ │ float value = buffer.getSample(0); │ │
│ │ REQUIRE(value == 0.5f); │ │
│ │ }); │ │
│ │ │ │
│ │ writer.join(); │ │
│ │ reader.join(); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ VENTAJAS: │
│ • Determinístico │
│ • Testea threading real si necesario │
│ │
│ DESVENTAJAS: │
│ • Más complejo │
│ • Más lento │
│ │
└─────────────────────────────────────────────────────────────────────┘
⏰ PATRÓN 2: Timing Dependencies¶
╔═════════════════════════════════════════════════════════════════════╗
║ ANTIPATRÓN: sleep() como mecanismo de sincronización ║
╚═════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ❌ BAD EXAMPLE │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("async_operation") { │ │
│ │ AsyncProcessor processor; │ │
│ │ │ │
│ │ processor.startAsync(); │ │
│ │ │ │
│ │ // ⚠️ "Should be enough time" │ │
│ │ std::this_thread::sleep_for(100ms); │ │
│ │ │ │
│ │ REQUIRE(processor.isComplete()); // Puede fallar │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ PROBLEMA: │
│ • En máquina lenta: 100ms no es suficiente → falla │
│ • En máquina rápida: 100ms es desperdicio → test lento │
│ • No hay garantía de timing │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ✅ GOOD SOLUTION: waitUntil con condición │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ // Helper function │ │
│ │ template<typename Condition> │ │
│ │ bool waitUntil(Condition cond, │ │
│ │ std::chrono::milliseconds timeout) { │ │
│ │ auto start = std::chrono::steady_clock::now(); │ │
│ │ while (!cond()) { │ │
│ │ auto elapsed = std::chrono::steady_clock::now() │ │
│ │ - start; │ │
│ │ if (elapsed > timeout) { │ │
│ │ return false; // Timeout │ │
│ │ } │ │
│ │ std::this_thread::sleep_for(10ms); // Poll │ │
│ │ } │ │
│ │ return true; // Condition met │ │
│ │ } │ │
│ │ │ │
│ │ TEST_CASE("async_operation_robust") { │ │
│ │ AsyncProcessor processor; │ │
│ │ processor.startAsync(); │ │
│ │ │ │
│ │ bool completed = waitUntil( │ │
│ │ [&]{ return processor.isComplete(); }, │ │
│ │ 5s // Max timeout │ │
│ │ ); │ │
│ │ │ │
│ │ REQUIRE(completed); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ VENTAJAS: │
│ • Rápido en máquina rápida (retorna apenas condition = true) │
│ • Robusto en máquina lenta (espera hasta timeout) │
│ • Explicit timeout evita hang infinito │
│ │
└─────────────────────────────────────────────────────────────────────┘
🌐 PATRÓN 3: External Dependencies¶
╔═════════════════════════════════════════════════════════════════════╗
║ ANTIPATRÓN: Depender de estado externo ║
╚═════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ❌ BAD EXAMPLE: Filesystem dependency │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("load_preset") { │ │
│ │ PresetManager manager; │ │
│ │ │ │
│ │ // ⚠️ Asume que este archivo existe │ │
│ │ auto preset = manager.load("/home/user/preset.json"); │ │
│ │ │ │
│ │ REQUIRE(preset.getName() == "My Preset"); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ PROBLEMA: │
│ • Falla si archivo no existe │
│ • Falla en diferentes máquinas/CI │
│ • Falla si path relativo vs absoluto │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ✅ GOOD SOLUTION: Test fixtures │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("load_preset_from_fixture") { │ │
│ │ PresetManager manager; │ │
│ │ │ │
│ │ // Usar fixture en directorio de tests │ │
│ │ std::filesystem::path fixtureDir = TEST_DATA_DIR; │ │
│ │ auto presetPath = fixtureDir / "test_preset.json"; │ │
│ │ │ │
│ │ // Verificar que fixture existe │ │
│ │ REQUIRE(std::filesystem::exists(presetPath)); │ │
│ │ │ │
│ │ auto preset = manager.load(presetPath); │ │
│ │ REQUIRE(preset.getName() == "Test Preset"); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ VENTAJAS: │
│ • Fixtures versionadas en repo │
│ • Funcionan en cualquier máquina │
│ • CI tiene acceso a fixtures │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ❌ BAD EXAMPLE: Network dependency │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("download_preset") { │ │
│ │ PresetDownloader downloader; │ │
│ │ │ │
│ │ // ⚠️ Real HTTP call │ │
│ │ auto preset = downloader.fetch( │ │
│ │ "https://example.com/preset.json" │ │
│ │ ); │ │
│ │ │ │
│ │ REQUIRE(preset.isValid()); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ PROBLEMA: │
│ • Falla si no hay internet │
│ • Falla si servidor está down │
│ • Lento (network latency) │
│ • No determinístico │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ✅ GOOD SOLUTION: Mock HTTP client │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ class MockHttpClient : public IHttpClient { │ │
│ │ public: │ │
│ │ std::string fetch(const std::string& url) override { │ │
│ │ // Return canned response │ │
│ │ return R"({"name": "Test Preset"})"; │ │
│ │ } │ │
│ │ }; │ │
│ │ │ │
│ │ TEST_CASE("download_preset_mocked") { │ │
│ │ auto mockClient = std::make_shared<MockHttpClient>(); │ │
│ │ PresetDownloader downloader(mockClient); │ │
│ │ │ │
│ │ auto preset = downloader.fetch("http://fake.url"); │ │
│ │ │ │
│ │ REQUIRE(preset.getName() == "Test Preset"); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ VENTAJAS: │
│ • Determinístico │
│ • Rápido (no network) │
│ • Funciona offline │
│ │
└─────────────────────────────────────────────────────────────────────┘
🎰 PATRÓN 4: Random Data¶
╔═════════════════════════════════════════════════════════════════════╗
║ ANTIPATRÓN: Usar random() sin seed fijo ║
╚═════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ❌ BAD EXAMPLE │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("process_random_data") { │ │
│ │ AudioBuffer buffer(512); │ │
│ │ │ │
│ │ // ⚠️ Different data cada run │ │
│ │ for (int i = 0; i < 512; ++i) { │ │
│ │ buffer[i] = (float)rand() / RAND_MAX; │ │
│ │ } │ │
│ │ │ │
│ │ Processor processor; │ │
│ │ processor.process(buffer); │ │
│ │ │ │
│ │ // Esta assertion puede pasar o fallar aleatoriamente │ │
│ │ REQUIRE(buffer.getRMS() < 0.5f); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ PROBLEMA: │
│ • No reproducible │
│ • Falla esporádicamente │
│ • Debugging imposible │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ✅ GOOD SOLUTION 1: Seed fijo │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("process_random_data_deterministic") { │ │
│ │ srand(42); // ✅ Fixed seed │ │
│ │ │ │
│ │ AudioBuffer buffer(512); │ │
│ │ for (int i = 0; i < 512; ++i) { │ │
│ │ buffer[i] = (float)rand() / RAND_MAX; │ │
│ │ } │ │
│ │ │ │
│ │ // Ahora same data cada run → reproducible │ │
│ │ Processor processor; │ │
│ │ processor.process(buffer); │ │
│ │ │ │
│ │ REQUIRE(buffer.getRMS() == Approx(0.287).margin(0.001)); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ✅ GOOD SOLUTION 2: Valores determinísticos (mejor) │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TEST_CASE("process_known_signal") { │ │
│ │ AudioBuffer buffer(512); │ │
│ │ │ │
│ │ // ✅ Explicit, conocido, reproducible │ │
│ │ float frequency = 440.0f; │ │
│ │ float sampleRate = 48000.0f; │ │
│ │ │ │
│ │ for (int i = 0; i < 512; ++i) { │ │
│ │ float phase = 2.0f * M_PI * frequency * i │ │
│ │ / sampleRate; │ │
│ │ buffer[i] = std::sin(phase); │ │
│ │ } │ │
│ │ │ │
│ │ Processor processor; │ │
│ │ processor.process(buffer); │ │
│ │ │ │
│ │ // Expectations basadas en matemáticas conocidas │ │
│ │ REQUIRE(buffer.getRMS() == Approx(0.707).margin(0.01)); │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ VENTAJAS: │
│ • Completamente determinístico │
│ • Matemáticamente verificable │
│ • Fácil de entender y mantener │
│ │
└─────────────────────────────────────────────────────────────────────┘
💻 PATRÓN 5: Platform-Specific Issues¶
╔═════════════════════════════════════════════════════════════════════╗
║ ANTIPATRÓN: Assumptions platform-specific ║
╚═════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ Problemas comunes: │
│ • Float rounding differences (x86 vs ARM) │
│ • Endianness (big endian vs little endian) │
│ • Path separators (/ vs \) │
│ • Line endings (LF vs CRLF) │
│ • sizeof(long) diferente (32-bit vs 64-bit) │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ❌ BAD: Float exact comparison │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ float result = someComputation(); │ │
│ │ REQUIRE(result == 0.3f); // ⚠️ Puede fallar por rounding │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ✅ GOOD: Approximate comparison │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ float result = someComputation(); │ │
│ │ REQUIRE_THAT(result, Catch::Matchers::WithinAbs(0.3f, 1e-6));│ │
│ │ // O: │ │
│ │ REQUIRE(result == Approx(0.3f).margin(1e-6)); │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
💡 Detection: Cómo Encontrar Flaky Tests¶
╔═════════════════════════════════════════════════════════════════════╗
║ ║
║ 🔍 TÉCNICA: Run test múltiples veces ║
║ ║
║ Si test pasa 99/100 runs → Es flaky, aunque parezca "casi 100%" ║
║ ║
╚═════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ Bash script: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ #!/bin/bash │ │
│ │ for i in {1..100}; do │ │
│ │ echo "Run $i/100" │ │
│ │ ./test_executable || { │ │
│ │ echo "FLAKY TEST DETECTED at run $i" │ │
│ │ exit 1 │ │
│ │ } │ │
│ │ done │ │
│ │ echo "Test is stable (100/100 passed)" │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ PowerShell: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 1..100 | ForEach-Object { │ │
│ │ Write-Host "Run $_/100" │ │
│ │ & .\test_executable.exe │ │
│ │ if ($LASTEXITCODE -ne 0) { │ │
│ │ Write-Error "FLAKY TEST at run $_" │ │
│ │ exit 1 │ │
│ │ } │ │
│ │ } │ │
│ │ Write-Host "Test is stable (100/100 passed)" │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
🛡️ Prevention: Evitar Flaky Tests¶
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ CHECKLIST: Code Review │
│ │
│ □ ¿Usa random() con seed fijo? │
│ □ ¿Usa sleep()? → Reemplazar con waitUntil() │
│ │ ¿Depende de filesystem? → Usar fixtures │
│ □ ¿Depende de network? → Usar mocks │
│ □ ¿Depende de system time? → Inject clock │
│ □ ¿Multithreading sin sync? → Agregar sincronización o mock │
│ □ ¿Float exact comparison? → Usar Approx() │
│ □ ¿Assumptions sobre timing? → Explicit waits │
│ │
└─────────────────────────────────────────────────────────────────────┘