Todos os artigos

// Knowledge.log — 技術記事

Como recusar -DskipTests no diff do coding agent

JDK 25: mvn test -DskipTests sai 0 sem executar testes. Gate local recusa skipTests/maven.test.skip no comando, log e diff.

O coding agent termina a tarefa, roda o build, recebe BUILD SUCCESS e escreve "testes passando". Aí você abre o transcript e a linha executada foi mvn test -DskipTests. Ou o agente não mexeu no comando e colocou <maven.test.skip>true</maven.test.skip> no pom.xml. O Maven saiu com exit 0 e nenhum teste rodou. O JUnit nem foi carregado para dizer que estava tudo bem.

A saída é um gate pequeno, em Python stdlib, que recusa skipTests e maven.test.skip em três lugares: no comando que o agente rodou, no log capturado e no diff de pom.xml e .mvn/maven.config. Tudo abaixo foi executado neste host em 2026-10-21.

Onde isso entra no que já publicamos

Já cobrimos o recibo do Surefire, que exige um TEST-*.xml recente e zerado. Também tem o gate contra asserção afrouxada e @Disabled, que lê o diff de src/test, e o \*IT que o Failsafe nunca rodou num mvn test. O caso de hoje acontece antes de tudo isso. O teste não foi enfraquecido nem ficou numa fase posterior do ciclo. A suíte inteira foi desligada por uma flag, e a flag está escrita no comando ou no diff. É ali que o gate procura.

Ambiente

  • Temurin JDK 25.0.4, Maven 3.9.12, maven.compiler.release=25, sem --enable-preview
  • maven-compiler-plugin 3.16.0. O latest do Central é 4.0.0-beta-5, que ficou de fora por ainda ser beta.
  • maven-surefire-plugin 3.6.0
  • junit-bom / junit-jupiter 6.1.3
  • Packaging jar, sem Spring, sem Failsafe, sem Docker
  • Python 3, só biblioteca padrão, para o gate

skipTests e maven.test.skip não fazem a mesma coisa

A página Skipping Tests do Surefire mostra os dois caminhos. O skipTests pode ser configurado no POM com <skipTests>true</skipTests> ou passado na linha de comando com mvn install -DskipTests. O maven.test.skip aparece com um "if you absolutely must" e vai mais longe: pula também a compilação dos testes. A página diz que essa propriedade é respeitada pelo Surefire, pelo Failsafe e pelo Compiler Plugin, e o exemplo é mvn install -Dmaven.test.skip=true.

A referência do goal surefire:test separa os dois parâmetros:

parâmetrouser propertyefeito documentadopadrão
skipTests (desde 2.4)skipTestspula a execução, mas ainda compila os testesfalse
skipmaven.test.skippula a execução e a compilação dos testesfalse

O lado da compilação está no compiler:testCompile: o parâmetro skip, com user property maven.test.skip, faz o plugin não compilar as fontes de teste.

Na prática, -DskipTests mantém o AdderTest.class em target/test-classes, só que ninguém executa a classe. -Dmaven.test.skip=true nem gera o .class. O exit code é 0 nos dois casos.

Repro: um teste, cinco exit 0

O módulo é o menor possível. POM:

<?xml version="1.0" encoding="UTF-8"?>
<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>skip-tests-gate</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>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>6.1.3</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.16.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.6.0</version>
      </plugin>
    </plugins>
  </build>
</project>

Produção, em src/main/java/devdojo/Adder.java:

package devdojo;

public final class Adder {
  private Adder() {}

  public static int add(int a, int b) {
    return a + b;
  }
}

Teste, em src/test/java/devdojo/AdderTest.java:

package devdojo;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class AdderTest {

  @Test
  void onePlusOneEqualsTwo() {
    assertEquals(2, Adder.add(1, 1), "skipTestsGateUniqueString");
  }
}

Os comandos rodaram nesta ordem:

comandoexito que ficou em target/
mvn -q test0 (05:08:32Z)TEST-devdojo.AdderTest.xml com tests=1 failures=0 errors=0 skipped=0, testcase onePlusOneEqualsTwo
mvn -q test -DskipTests (sem clean)0 (05:08:36Z)teste não executado, XML anterior intacto, mesmo mtime 05:08:32.439Z
mvn -B clean test -DskipTests0 (05:08:39Z)AdderTest.class compilado, sem target/surefire-reports
mvn -q clean test -Dmaven.test.skip=true0sem target/test-classes, sem relatórios
mvn -B clean test -Dmaven.test.skip=true0 (05:08:44Z)idem, com o motivo no log

Com -B e sem -q, o Maven diz o que está fazendo. No clean test -DskipTests:

[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ skip-tests-gate ---
[INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ skip-tests-gate ---
[INFO] Tests are skipped.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

No clean test -Dmaven.test.skip=true, o skip começa antes:

[INFO] --- resources:3.3.1:testResources (default-testResources) @ skip-tests-gate ---
[INFO] Not copying test resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ skip-tests-gate ---
[INFO] Not compiling test sources
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ skip-tests-gate ---
[INFO] Tests are skipped.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

Duas armadilhas aparecem na tabela.

O -q esconde a única pista. Tests are skipped. é uma linha [INFO], e o modo quiet some com ela. Um agente que roda mvn -q test -DskipTests devolve um log vazio e exit 0, que é exatamente o que um build verde e silencioso devolveria. O quiet cumpre o que promete.

O XML velho continua lá. Sem clean, o -DskipTests não apaga target/surefire-reports. O TEST-devdojo.AdderTest.xml da execução das 05:08:32 continuou no disco com o mesmo mtime e failures=0. Quem lê o diretório sem olhar a idade do arquivo aceita como recibo uma execução que não é a da tarefa. Essa checagem de idade é o assunto do recibo de 22/09. Aqui o ponto é outro: dá para recusar a flag antes de abrir qualquer XML.

O gate: comando, log e diff

O gate procura tokens de texto. Não interpreta o modelo do Maven, não lê XML do Surefire e não procura @Disabled. Sai com 1 se encontrar algum token e com 0 se não encontrar nenhum.

#!/usr/bin/env python3
"""Fail closed when a coding-agent command, log, or diff introduces skipTests / maven.test.skip.

Does not parse Surefire XML as the primary receipt.
Does not inspect Failsafe / *IT / skipITs.
Does not look for @Disabled or loosened assertions.
"""
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

# Tokens that zero the unit-test suite without failing the Maven process.
# Keep skipITs out: that is Failsafe-only.
PATTERNS: list[tuple[str, re.Pattern[str]]] = [
    ("-DskipTests", re.compile(r"-DskipTests\b")),
    ("skipTests=true", re.compile(r"\bskipTests\s*=\s*true\b", re.IGNORECASE)),
    ("-Dmaven.test.skip", re.compile(r"-Dmaven\.test\.skip\b")),
    ("maven.test.skip=true", re.compile(r"\bmaven\.test\.skip\s*=\s*true\b", re.IGNORECASE)),
    ("<skipTests>true</skipTests>", re.compile(r"<skipTests>\s*true\s*</skipTests>", re.IGNORECASE)),
    (
        "<maven.test.skip>true</maven.test.skip>",
        re.compile(r"<maven\.test\.skip>\s*true\s*</maven\.test\.skip>", re.IGNORECASE),
    ),
]


def findings(text: str) -> list[str]:
    hits: list[str] = []
    for label, pattern in PATTERNS:
        if pattern.search(text):
            hits.append(label)
    return hits


def read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8", errors="replace")


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--command", default="", help="Agent command string")
    parser.add_argument("--log", type=Path, help="Captured Maven / agent log")
    parser.add_argument("--diff", type=Path, help="Unified diff (pom.xml / .mvn/maven.config)")
    parser.add_argument("--file", action="append", default=[], type=Path, help="File contents to scan")
    args = parser.parse_args()

    problems: list[str] = []
    if args.command:
        for hit in findings(args.command):
            problems.append(f"command: {hit}")
    if args.log is not None:
        for hit in findings(read_text(args.log)):
            problems.append(f"log {args.log}: {hit}")
    if args.diff is not None:
        for hit in findings(read_text(args.diff)):
            problems.append(f"diff {args.diff}: {hit}")
    for path in args.file:
        for hit in findings(read_text(path)):
            problems.append(f"file {path}: {hit}")

    if problems:
        for item in problems:
            print("FAIL:", item)
        return 1
    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Uso básico:

python3 gate.py --command "mvn -q test"                 # exit 0
python3 gate.py --command "mvn -q test -DskipTests"     # exit 1
python3 gate.py --diff pom-skipTests.diff               # exit 1

Resultado de todas as execuções neste host:

entradaexitsaída
--command "mvn -q test -DskipTests"1FAIL: command: -DskipTests
--command "mvn test -DskipTests=true"1FAIL: command: -DskipTests
--command "mvn -q test -Dmaven.test.skip=true"1FAIL: command: -Dmaven.test.skip
--log de agente com o comando de skip registrado1FAIL: log ...: -Dmaven.test.skip
--diff com <skipTests>true</skipTests> no Surefire1FAIL: diff ...: <skipTests>true</skipTests>
--diff com <maven.test.skip>true</maven.test.skip> em <properties>1FAIL: diff ...: <maven.test.skip>true</maven.test.skip>
--diff de .mvn/maven.config com -DskipTests1FAIL: diff ...: -DskipTests
--command "mvn -q test"0PASS
--log de agente limpo0PASS
--diff de POM sem skip0PASS
--file pom.xml (o POM acima)0PASS
--command "mvn -q test -DskipITs"0PASS

Por onde a flag entra

O agente não precisa digitar -DskipTests no terminal. Os caminhos que o gate cobre:

  • Linha de comando. -DskipTests, -DskipTests=true, -Dmaven.test.skip=true.
  • pom.xml. Pode ser <skipTests>true</skipTests> na configuração do Surefire, que é a forma da página oficial, ou <skipTests>/<maven.test.skip> em <properties>, inclusive dentro de um profile.
  • .mvn/maven.config. No Maven 3.9 o arquivo tem um argumento por linha. Um -DskipTests adicionado ali vale para todo mvn rodado no projeto, inclusive o seu.
  • MAVEN_ARGS. A partir do Maven 3.9.0, o script mvn lê essa variável e pode receber -DskipTests por ela. Já MAVEN_OPTS é para opções da JVM. Como o gate só lê texto, o mais simples é concatenar o ambiente no --command e deixar o regex resolver.

No harness, o esboço fica assim (este trecho de shell não foi executado neste run):

git diff "$BASE" -- pom.xml .mvn/maven.config > agent.diff
python3 gate.py \
  --command "$AGENT_MVN_CMD ${MAVEN_ARGS:-} ${MAVEN_OPTS:-}" \
  --log agent.log \
  --diff agent.diff \
  || { echo "tarefa recusada: suite desligada por flag" >&2; exit 1; }

O git diff "$BASE" -- ... sem segundo commit compara o working tree com o base. Assim entra também a mudança que o agente deixou sem commitar, que costuma ser justamente a que ninguém revisou.

Vale um detalhe sobre o log. A saída do Maven com -B não repete a flag, só imprime Tests are skipped.. O --log pega o token quando o log do agente registra o comando executado, como no fixture da tabela. Se o seu harness guarda só o stdout do Maven, o --command é o que pega a flag. Acrescentar um padrão para Tests are skipped. é uma extensão razoável, mas ela não fez parte deste gate nem dos runs acima, e com -q a linha nem aparece.

Limites do gate

  • Failsafe fica de fora de propósito. O -DskipITs passa no gate porque só afeta o Failsafe, e a página de skipping do Failsafe avisa que, desde o 3.6.0, -DskipTests não pula mais os ITs. Esse caso está no artigo do \*IT e do Failsafe.
  • XML ausente ou velho é assunto do recibo do Surefire. @Disabled e assert afrouxado são do gate de diff de src/test. Os três gates se somam, nenhum substitui o outro.
  • -DskipTests=false também reprova. O regex -DskipTests\b não olha o valor. Isso é intencional, porque um harness de agente deve falhar fechado. Se o projeto usa o padrão da documentação, com skip ligado por padrão no POM e religado com -DskipTests=false, coloque exatamente essa string numa allowlist explícita do harness. Afrouxar o regex não resolve.
  • Token não é modelo. Um comentário no POM com <skipTests>true</skipTests> dispara o gate. Uma linha removida no diff também, porque o gate lê o diff inteiro. Os dois casos dão falso positivo do lado seguro. No sentido contrário, um <skip>true</skip> direto na configuração do Surefire não bate com nenhum padrão. Se isso aparece no seu repositório, adicione o padrão.

Próximo passo

Coloque o gate.py ao lado do harness do agente e rode o gate antes de a tarefa ser marcada como testada, passando o comando (com MAVEN_ARGS), o log e o git diff de pom.xml e .mvn/maven.config contra o commit base. Se sair 1, a tarefa volta para o agente com o stdout do gate e não vira PR. A recomendação da DevDojo é recusar sempre e liberar só pela allowlist que uma pessoa aprovou. Enquanto o comando tiver skipTests ou maven.test.skip, um BUILD SUCCESS não mostra nada sobre os testes, porque eles não rodaram.

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