Todos os artigos

// Knowledge.log — 技術記事

mvn test no coding agent: o *IT que o Failsafe nunca rodou

JDK 25, Failsafe 3.6.0: mvn test sai verde sem rodar *IT.java e sem failsafe-reports. Com *IT no módulo, o gate do agente é mvn verify.

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-plugin 3.6.0, maven-failsafe-plugin 3.6.0
  • org.junit.jupiter:junit-jupiter 6.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:

comandoexitsurefire-reportsfailsafe-reports
mvn -q test0TEST-devdojo.UnitProbeTest.xmldiretório ausente
mvn -q clean verify0TEST-devdojo.UnitProbeTest.xmlTEST-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 fase integration-test, para que post-integration-test consiga rodar. Quem quebra o build é o failsafe:verify. Por isso a própria documentação recomenda chamar mvn verify em vez de chamar a fase integration-test direto.
  • Goal isolado não é atalho. No mesmo experimento, rodar failsafe:integration-test failsafe:verify depois de clean saiu com exit 0 e não gerou nenhum TEST-*.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):

  • -DskipITs pula os testes de integração do Failsafe.
  • A partir do Failsafe 3.6.0, -DskipTests pula só o Surefire e não pula mais os ITs do Failsafe.
  • -Dmaven.test.skip=true vale 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.

javamavenai-agents

// Continue.training — 次のステップ

Conhecimento só conta quando vira prática.

Volte ao artigo, execute os exemplos e compartilhe o que aprendeu.

Explorar mais artigos