Skip to content

⚡ 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                     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘