O coding agent termina a tarefa, roda mvn test, recebe exit 0 e escreve: "todos os testes passaram". Nos unit tests isso é verdade. Só que o módulo também tem uma classe *IT.java, e ela não rodou. Não falhou e não foi pulada com aviso. O Maven nem chegou à fase em que ela roda.
Aqui você vai ver por que isso acontece e o que fica (e o que falta) em target/. Também vai sair com um gate de agente que não aceita esse verde pela metade. Tudo foi executado num módulo mínimo, sem container e sem framework.
Onde isso encosta no que já publicamos
Já escrevemos sobre usar o XML do Surefire como recibo de que os testes rodaram, sobre o gate de diff contra asserção afrouxada e @Disabled e sobre os gates do repositório antes do primeiro PR. O caso de hoje é outro falso verde. O XML do Surefire existe, está verde e é legítimo. O que falta é o recibo de outro plugin, que ninguém conferiu.
Ambiente
- Temurin JDK 25.0.4, Maven 3.9.12,
maven.compiler.release=25, sem--enable-preview maven-surefire-plugin3.6.0,maven-failsafe-plugin3.6.0org.junit.jupiter:junit-jupiter6.1.3- Packaging
jar, sem Spring, sem Testcontainers, sem HTTP
Dois plugins, dois padrões de nome, fases diferentes
O Surefire e o Failsafe são da mesma família de plugins, mas cada um procura classes com nomes diferentes.
Surefire 3.6.0, includes padrão (documentação de inclusão do Surefire):
**/Test*.java**/*Test.java**/*Tests.java**/*TestCase.java
Failsafe 3.6.0, includes padrão (documentação de inclusão do Failsafe):
**/IT*.java**/*IT.java**/*ITCase.java
IntegrationProbeIT.java não bate com nenhum padrão do Surefire. O Surefire 3.x continua sem incluir *IT.java por padrão.
Os dois plugins também rodam em fases diferentes do ciclo de vida do Maven. O surefire:test se liga à fase test. O failsafe:integration-test se liga a integration-test e o failsafe:verify se liga a verify. A ordem é esta:
... test → package → pre-integration-test → integration-test → post-integration-test → verify
mvn test para em test. Tudo o que vem depois, inclusive o Failsafe, fica de fora.
Tem um detalhe que pega muita gente. Mesmo com mvn verify, o Failsafe só roda se estiver declarado no POM. No Maven 3.9.12, o Super POM não traz o Failsafe, e as bindings padrão do packaging jar só ligam o Surefire (na versão 3.2.5) à fase test. Não existe nenhum plugin ligado a integration-test ou a verify. Por isso o POM do exemplo fixa as duas versões e declara as execuções do Failsafe:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>devdojo</groupId>
<artifactId>failsafe-it-probe</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Os goals não declaram <phase> porque herdam a fase padrão (integration-test e verify). Sem esse bloco, mvn verify passa por cima do *IT do mesmo jeito que mvn test.
Os dois testes são de propósito pequenos. O unit test:
package devdojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class UnitProbeTest {
@Test
void unitProbeOnePlusOneEqualsTwo() {
assertEquals(2, 1 + 1, "unitProbeUniqueString");
}
}
E o teste de integração, que escreve e lê um arquivo temporário, sem rede e sem servidor:
package devdojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class IntegrationProbeIT {
@Test
void integrationProbeTempFileRoundTrip() throws Exception {
Path tmp = Files.createTempFile("failsafe-it-probe-", ".txt");
Files.writeString(tmp, "integrationProbeUniqueString", StandardCharsets.UTF_8);
assertEquals(
"integrationProbeUniqueString",
Files.readString(tmp, StandardCharsets.UTF_8));
}
}
O Failsafe não precisa de container de servlet. O exemplo com Jetty na documentação é opcional. Qualquer classe com o nome no padrão e um teste JUnit serve.
O que aparece em target/ depois de cada comando
Os dois comandos rodaram no mesmo módulo, e depois deles conferimos os diretórios de relatório:
| comando | exit | surefire-reports | failsafe-reports |
|---|---|---|---|
mvn -q test | 0 | TEST-devdojo.UnitProbeTest.xml | diretório ausente |
mvn -q clean verify | 0 | TEST-devdojo.UnitProbeTest.xml | TEST-devdojo.IntegrationProbeIT.xml |
Depois de mvn -q test:
$ ls target/failsafe-reports
ls: cannot access 'target/failsafe-reports': No such file or directory
O diretório do Failsafe não está vazio, ele nem existe. Em modo quiet, a saída do mvn -q test só trouxe avisos de sun.misc.Unsafe vindos do Guava que o próprio Maven carrega. Nada ali sugere que faltou alguma coisa.
Depois de mvn -q clean verify, cada plugin registra só o que é dele:
<!-- target/surefire-reports/TEST-devdojo.UnitProbeTest.xml -->
<testcase name="unitProbeOnePlusOneEqualsTwo" classname="devdojo.UnitProbeTest" time="0.035"/>
<!-- target/failsafe-reports/TEST-devdojo.IntegrationProbeIT.xml -->
<testcase name="integrationProbeTempFileRoundTrip" classname="devdojo.IntegrationProbeIT" time="0.05"/>
O XML do Surefire não menciona IntegrationProbeIT, e o do Failsafe não menciona UnitProbeTest. O Failsafe ainda grava failsafe-summary.xml:
<completed>1</completed>
<errors>0</errors>
<failures>0</failures>
<skipped>0</skipped>
O recibo do *IT está em target/failsafe-reports. Ler só surefire-reports, por mais verde que ele esteja, não diz nada sobre o teste de integração.
O script do agente e as flags de skip
O gate que costuma aparecer no harness do agente é este:
#!/usr/bin/env bash
set -euo pipefail
mvn -q test
echo "todos os testes passaram"
O set -euo pipefail deixa o script com cara de rigoroso, e ele é. Só que é rigoroso com o comando errado. O exit 0 vale para o Surefire. A frase do echo vai além do que o comando provou, e o agente repete essa frase no resumo do PR.
Duas coisas do Failsafe mudam como esse gate deve ser escrito:
- A falha só aparece no
verify. Segundo a documentação do Failsafe, o plugin não quebra o build na faseintegration-test, para quepost-integration-testconsiga rodar. Quem quebra o build é ofailsafe:verify. Por isso a própria documentação recomenda chamarmvn verifyem vez de chamar a faseintegration-testdireto. - Goal isolado não é atalho. No mesmo experimento, rodar
failsafe:integration-test failsafe:verifydepois decleansaiu com exit 0 e não gerou nenhumTEST-*.xml, porque não havia teste compilado. Mais um verde sem nada dentro.
E as flags de skip, que mudaram no 3.6.0 (skipping tests no Failsafe):
-DskipITspula os testes de integração do Failsafe.- A partir do Failsafe 3.6.0,
-DskipTestspula só o Surefire e não pula mais os ITs do Failsafe. -Dmaven.test.skip=truevale para Surefire, Failsafe e Compiler. Ele pula a compilação e a execução dos testes.
Se o comando do agente tiver qualquer uma dessas três flags, ele não está testando o que diz testar.
O gate que dá para mostrar em review
O gate honesto roda o ciclo até verify e confere o recibo quando o módulo declara *IT:
#!/usr/bin/env bash
set -euo pipefail
mvn -q clean verify
has_it="$(find src/test/java \( -name '*IT.java' -o -name 'IT*.java' -o -name '*ITCase.java' \) -print -quit)"
if [ -n "$has_it" ]; then
compgen -G 'target/failsafe-reports/TEST-*.xml' > /dev/null || {
echo "módulo tem *IT, mas target/failsafe-reports não tem TEST-*.xml" >&2
exit 1
}
fi
echo "unit tests (Surefire) e integration tests (Failsafe) executados"
Os padrões do find são os mesmos includes padrão do Failsafe. Se o seu POM muda os includes, o script tem que acompanhar. Em módulo multi-módulo, rode a verificação por módulo.
No CI vale a mesma regra. Publique target/failsafe-reports como artefato e faça o job falhar se o módulo tiver *IT e o diretório não tiver TEST-*.xml. Esse é o sinal que você consegue observar em produção. O exit code do Maven sozinho não distingue "ITs passaram" de "ITs nunca rodaram", e a ausência do diretório distingue.
Próximo passo
Se o módulo tem *IT.java, o gate do agente é mvn verify com o Failsafe declarado no POM e o recibo em target/failsafe-reports conferido. mvn test não serve. Se o módulo não tem *IT nem Failsafe, mvn test continua sendo o comando certo para unit tests. Só não deixe o agente resumir isso como "todos os testes".
Para começar, procure *IT.java no repositório e confira se o POM declara as execuções do Failsafe. Depois abra o script de gate do agente e troque test por verify. Se target/failsafe-reports aparecer pela primeira vez depois disso, você acabou de descobrir há quanto tempo esses testes não rodavam.