Pular para conteúdo

Diagnóstico de testes flaky

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15

Sintoma: o ./gradlew check do job gate do pipeline.yml falha sem alteração de código, e um re-run passa. O build instrumenta a suíte para que a falha diga qual teste caiu e por quê, e para que uma trava vire falha em vez de segurar o job.

Verbosidade de teste (sempre ligada)

build.gradle.kts — tasks.withType<Test> configura testLogging com:

  • events("failed", "skipped") — cada teste falhado ou pulado vai para o stdout.
  • exceptionFormat = FULL — stack trace inteiro, sem truncar.
  • showCauses = true — mostra os Caused by: das cadeias de JdbcException.

Sem isso o Gradle só diz There were failing tests, e o detalhe fica no HTML report do runner.

Retry automático no CI (test-retry)

build.gradle.kts — plugin org.gradle.test-retry 1.5.10.

Ligado só no CI, detectado pela env CI, que o GitHub Actions define em todo job. Configuração:

  • maxRetries=2 — cada teste falhado é re-executado até 2 vezes antes de virar falha real.
  • maxFailures=5 — se mais de 5 testes distintos falharem, a suíte aborta: não é flakiness, é bug ou regressão.
  • failOnPassedAfterRetry=false — retry que passa deixa a suíte verde, mas o teste sai marcado como flaky no HTML report e no console.

O retry não esconde o defeito: quando passa, o log do job lista o teste que precisou dele, e esse é o teste a investigar offline (timing, corrida no banco, porta). Quando esgota os retries, é bug real, com o stack trace completo no log.

Travas viram falha

Onde Teto Cobre
build.gradle.kts — tasks.test { timeout = Duration.ofMinutes(15) } 15 min a task inteira, inclusive a subida do contexto Micronaut
src/test/resources/junit-platform.properties — junit.jupiter.execution.timeout.default = 2 m 2 min cada teste e cada método de ciclo de vida; desligado sob debug (disabled_on_debug)
application-test.yml — datasources.default.connection-timeout: 5000 5 s cada pedido de conexão ao pool: container morto falha em 5 s, não em 30 s por teste

printTestSummary (resumo e testes lentos)

Task do build.gradle.kts, finalizedBy do test: roda sempre depois dele, inclusive quando falha. Lê os XMLs JUnit em build/test-results/test/*.xml e imprime:

>>> Test summary: 565 passed, 0 failed, 2 skipped (total 567) in 20,6s
>>> Slow tests (>5s, top 10):
     5475ms - br.com.xadm.comum.ApiBearerSecurityIntegrationTest#apiPost_withoutBearer_401()
>>> CI mode: retry habilitado (até 2 retries por teste). ...

Teste acima de 5 s entra na lista: é candidato a estourar prazo no runner, que tem menos CPU que a máquina do dev. O tempo somado é a soma do tempo de cada teste, não o relógio de parede. A task lê os arquivos no doLast sem capturar estado do script, e por isso é compatível com o configuration cache.

Postgres de teste fora do ar

Os @MicronautTest rodam contra o Postgres singleton da xadm-comum-teste (Perfis Micronaut). Com o Docker fora do ar, toda classe @MicronautTest falha com IllegalStateException: Postgres de teste (postgres:18-alpine) não subiu: confira se o Docker está no ar e acessível ao Testcontainers., com a causa original anexada. Os testes unitários passam no mesmo run. Não há serviço nem estado em disco a limpar entre runs: o container vive o tempo da JVM de teste e o Ryuk do Testcontainers o recolhe, inclusive depois de interrupção.

Como investigar quando voltar a acontecer

  1. No log do job gate, procure >>> Test summary: (quantos falharam), as linhas br.com.xadm.X > metodoY FAILED (quais) e flaked/retry (o que o plugin reexecutou).
  2. Se o mesmo teste aparece nos retries de vários runs, é flaky determinístico: reproduza local com ./gradlew test --tests "ClasseX.metodoY" --rerun-tasks, repetido até falhar.
  3. Se o teste muda a cada run, é infraestrutura do runner (Docker, recurso esgotado), não o código.

Para desligar o retry temporariamente

Se o retry estiver mascarando algo que precisa ser investigado:

// build.gradle.kts, dentro de tasks.withType<Test>
retry {
    if (false /* ciMode */) {  // desativa manualmente
        ...
    }
}

Localmente ele já fica desligado (sem a env CI).