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 osCaused by:das cadeias deJdbcException.
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 comoflakyno 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¶
- No log do job
gate, procure>>> Test summary:(quantos falharam), as linhasbr.com.xadm.X > metodoY FAILED(quais) eflaked/retry(o que o plugin reexecutou). - 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. - 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).