Todos os artigos

// Knowledge.log — 技術記事

Instant.now(): o teste de prazo que nunca falha de manhã

JDK 25: Instant.now() no domínio deixa o teste de prazo no relógio da máquina. Clock.systemUTC() em produção e Clock.fixed no JUnit. 8 testes, 0 falhas.

A regra parece simples: a fatura vence às 08:00 UTC e, depois desse horário, está atrasada. O código de domínio chama Instant.now(), compara com o vencimento e devolve um boolean. O teste roda verde às 07h. Depois do almoço, a mesma linha fica vermelha sem que ninguém tenha mexido em nada. Com LocalDate.now() é ainda pior, porque o resultado também muda na virada do dia e quando o teste roda numa máquina com outro fuso.

As saídas de sempre são desligar a asserção, trocar assertTrue por "tanto faz" ou colocar um Thread.sleep até a meia-noite. O sleep até funciona, desde que o CI aceite um job de dezoito horas.

A correção é injetar java.time.Clock pelo construtor, usar Clock.systemUTC() em produção e Clock.fixed(...) no teste. Abaixo estão o teste ingênuo falhando neste host, o mesmo prazo fixado com Clock.fixed e o ponto em que o Clock deixa de ajudar.

Ambiente

  • Temurin 25.0.4 (25.0.4+7-LTS), sem --enable-preview
  • Maven 3.9.12, maven.compiler.release=25
  • maven-compiler-plugin 3.14.0, maven-surefire-plugin 3.6.0
  • org.junit.jupiter:junit-jupiter 6.1.3
  • Fuso do host: Etc/UTC. Execução por volta das 05:13 UTC de 2026-10-26.

O pom.xml, resumido:

<groupId>devdojo</groupId>
<artifactId>deadline-clock-fixture</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
  <maven.compiler.release>25</maven.compiler.release>
  <junit.version>6.1.3</junit.version>
</properties>
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
  </dependency>
</dependencies>
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.14.0</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.6.0</version>
      <configuration>
        <excludes>
          <exclude>**/NaiveWallClockBoundaryTest.java</exclude>
        </excludes>
      </configuration>
    </plugin>
  </plugins>
</build>

O teste ingênuo fica fora do Surefire padrão de propósito. Ele existe para falhar, e só roda quando pedido explicitamente.

O que o javadoc diz sobre now() sem argumento

O javadoc do JDK 25 é bem direto. Sobre o Instant.now()), ele diz que o método consulta o system UTC clock e que usá-lo "will prevent the ability to use an alternate time-source for testing because the clock is effectively hard-coded".

O LocalDate.now()) é parecido, com um agravante: ele usa o system clock in the default time-zone. O relógio continua fixo no código, e o fuso padrão da JVM também passa a influenciar o resultado.

As duas classes têm uma sobrecarga que recebe um relógio: Instant.now(Clock) e LocalDate.now(Clock). O javadoc de cada uma diz que ela permite usar um relógio alternativo em teste e que esse relógio pode ser fornecido por dependency injection. A visão geral de Clock chama de boa prática passar um Clock para qualquer método que precise do instante atual e do fuso.

Na prática, uma regra de vencimento que chama now() sem argumento depende da hora e do fuso da máquina onde roda. Por isso o resultado muda na virada do dia ou do fuso.

O domínio que lê o relógio da máquina

Esta é a versão que muita gente tem em produção:

package devdojo.deadline;

import java.time.Instant;
import java.time.LocalDate;

public final class WallClockInvoice {
    private final Instant dueInstant;
    private final LocalDate dueDate;

    public WallClockInvoice(Instant dueInstant, LocalDate dueDate) {
        this.dueInstant = dueInstant;
        this.dueDate = dueDate;
    }

    public boolean isOverdueInstant() {
        return Instant.now().isAfter(dueInstant);
    }

    public boolean isOverdueDate() {
        return LocalDate.now().isAfter(dueDate);
    }
}

O único construtor não recebe Clock, e a suíte confere isso por reflexão (wallClockInvoiceHasNoClockConstructor). O teste não tem como escolher a hora.

O teste ingênuo tenta mesmo assim. Ele espera que a fatura esteja atrasada "como se fossem 09:00Z" e que, pela data, ainda não esteja atrasada "como se fosse dia 25 em São Paulo":

class NaiveWallClockBoundaryTest {

    @Test
    void assertsOverdueAtNineAmButDomainCallsInstantNow() {
        Instant due = Instant.parse("2026-10-26T08:00:00Z");
        WallClockInvoice invoice = new WallClockInvoice(due, LocalDate.of(2026, 10, 25));
        assertTrue(invoice.isOverdueInstant());
    }

    @Test
    void assertsNotOverdueOnSaoPauloOctober25ButDomainCallsLocalDateNow() {
        WallClockInvoice invoice = new WallClockInvoice(
                Instant.parse("2026-10-26T08:00:00Z"), LocalDate.of(2026, 10, 25));
        assertFalse(invoice.isOverdueDate());
    }
}

O "como se fossem 09:00Z" só existe na cabeça de quem escreveu o teste. A JVM não leu esse comentário.

mvn -q test -Dtest=NaiveWallClockBoundaryTest

Resultado neste host: tests=2, failures=2, errors=0.

  • assertsOverdueAtNineAmButDomainCallsInstantNow: expected: <true> but was: <false>. Eram umas 05:13Z, antes das 08:00Z, então a fatura ainda não estava vencida.
  • assertsNotOverdueOnSaoPauloOctober25ButDomainCallsLocalDateNow: expected: <false> but was: <true>. O fuso do host é Etc/UTC, então LocalDate.now() devolveu 2026-10-26, que é depois de 25.

Rodando o mesmo arquivo depois das 08:00Z, o primeiro teste passa. Ninguém conserta nada: o relógio andou, e o resultado mudou junto.

Também não adianta calcular a expectativa com Instant.now() dentro do próprio teste. O domínio e o teste vão ler o mesmo relógio da máquina, cada um num momento diferente, e o teste só confirma que o relógio existe.

O mesmo prazo com Clock.fixed

A regra é a mesma. A única mudança é que o relógio entra pelo construtor:

package devdojo.deadline;

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

public final class ClockInvoice {
    private final Instant dueInstant;
    private final LocalDate dueDate;
    private final Clock clock;

    public ClockInvoice(Instant dueInstant, LocalDate dueDate, Clock clock) {
        this.dueInstant = dueInstant;
        this.dueDate = dueDate;
        this.clock = clock;
    }

    public static ClockInvoice productionUtc(Instant dueInstant, LocalDate dueDate) {
        return new ClockInvoice(dueInstant, dueDate, Clock.systemUTC());
    }

    public static ClockInvoice productionInZone(Instant dueInstant, LocalDate dueDate, ZoneId zone) {
        return new ClockInvoice(dueInstant, dueDate, Clock.system(zone));
    }

    public boolean isOverdueInstant() {
        return Instant.now(clock).isAfter(dueInstant);
    }

    public boolean isOverdueDate() {
        return LocalDate.now(clock).isAfter(dueDate);
    }
}

No teste, você escolhe o instante e o fuso:

class ClockDeadlineTest {

    private static final Instant DUE_INSTANT = Instant.parse("2026-10-26T08:00:00Z");
    private static final LocalDate DUE_DATE = LocalDate.of(2026, 10, 25);
    private static final ZoneId SAO_PAULO = ZoneId.of("America/Sao_Paulo");

    @Test
    void fixedClockPinsInstantOverdueIndependentOfHostTime() {
        Clock seven = Clock.fixed(Instant.parse("2026-10-26T07:00:00Z"), ZoneOffset.UTC);
        Clock nine = Clock.fixed(Instant.parse("2026-10-26T09:00:00Z"), ZoneOffset.UTC);
        assertFalse(new ClockInvoice(DUE_INSTANT, DUE_DATE, seven).isOverdueInstant());
        assertTrue(new ClockInvoice(DUE_INSTANT, DUE_DATE, nine).isOverdueInstant());
    }

    @Test
    void sameInstantLocalDateFlipsAcrossSaoPauloBoundaryWithFixedClock() {
        Instant twoAm = Instant.parse("2026-10-26T02:00:00Z");
        Clock beforeMidnightSp = Clock.fixed(twoAm, SAO_PAULO);
        Clock afterMidnightSp = Clock.fixed(Instant.parse("2026-10-26T03:00:00Z"), SAO_PAULO);
        Clock sameInstantUtc = Clock.fixed(twoAm, ZoneOffset.UTC);

        assertEquals(LocalDate.of(2026, 10, 25), LocalDate.now(beforeMidnightSp));
        assertEquals(LocalDate.of(2026, 10, 26), LocalDate.now(sameInstantUtc));
        assertEquals(Instant.now(beforeMidnightSp), Instant.now(sameInstantUtc));

        assertFalse(new ClockInvoice(DUE_INSTANT, DUE_DATE, beforeMidnightSp).isOverdueDate());
        assertTrue(new ClockInvoice(DUE_INSTANT, DUE_DATE, afterMidnightSp).isOverdueDate());
        assertTrue(new ClockInvoice(DUE_INSTANT, DUE_DATE, sameInstantUtc).isOverdueDate());
    }
}
mvn -q test

Resultado: ClockDeadlineTest com tests=8, failures=0, errors=0, skipped=0.

Com vencimento em 2026-10-26T08:00:00Z (regra por instante) e 2026-10-25 (regra por data), estes foram os valores observados:

RelógioRegraValor lidoVencido?
Clock.fixed(07:00Z, UTC)instante vs 08:00ZInstant 07:00Zfalse
Clock.fixed(09:00Z, UTC)instante vs 08:00ZInstant 09:00Ztrue
Clock.fixed(02:00Z, America/Sao_Paulo)data vs 2026-10-25Instant 02:00Z, LocalDate 2026-10-25false
Clock.fixed(03:00Z, America/Sao_Paulo)data vs 2026-10-25LocalDate 2026-10-26true
Clock.fixed(02:00Z, UTC)data vs 2026-10-25Instant 02:00Z, LocalDate 2026-10-26true
Relógio da máquina, ~05:13Z, Etc/UTCinstante vs 08:00ZInstant.now()false
Relógio da máquina, ~05:13Z, Etc/UTCdata vs 2026-10-25LocalDate.now() (26)true

Duas linhas pedem atenção. Às 02:00Z, o relógio de São Paulo e o relógio UTC devolvem o mesmo Instant, mas datas diferentes: 25 em São Paulo, que estava em UTC−3, e 26 em UTC. A regra por data muda de resultado sem ninguém esperar a meia-noite, porque basta trocar o ZoneId do Clock. E duas leituras seguidas de Instant.now(fixed) devolvem o mesmo valor. Clock.fixed sempre retorna o mesmo instante, e o javadoc o descreve como "not a clock in the conventional sense".

Uma observação sobre a própria suíte: dois dos oito testes, wallClockInstantOverdueCouplesToHostNowVersusPinnedNineAm e wallClockDateOverdueCouplesToDefaultZoneAndHostDate, comparam de propósito o WallClockInvoice com o relógio fixado. Eles servem de prova do que aconteceu naquele horário. Rodado depois das 08:00Z, o primeiro inverte. É o problema deste texto demonstrado dentro da suíte que mostra a solução. Os testes com Clock.fixed não têm esse problema.

Sobre o JUnit: a versão 6.1.3 não tem anotação para congelar o relógio. Não existe @FixedClock. O @DefaultTimeZone só muda o fuso padrão da JVM, e o Instant lido continua sendo o da máquina. O que resolve é o Clock.fixed passado pelo construtor.

Produção: Clock.systemUTC() ou um ZoneId de negócio

Escolha o relógio de produção pelo tipo de regra:

  • A regra compara instantes (venceu às 08:00Z?): use Clock.systemUTC(). O javadoc recomenda esse relógio, em vez de systemDefaultZone(), quando você precisa do instante atual sem data ou hora. É o ClockInvoice.productionUtc(...) acima. Na suíte, productionSystemUtcMatchesWallInstantOnThisHost confirma que ele fica a menos de 2 s do Instant.now() neste host.
  • A regra compara datas (venceu no dia 25?): use um Clock com o ZoneId do negócio declarado explicitamente, como Clock.system(ZoneId.of("America/Sao_Paulo")), que é o productionInZone(...). No teste, o equivalente é Clock.fixed(instant, zona). Evite Clock.systemDefaultZone() e LocalDate.now() sem argumento: os dois deixam o fuso padrão da JVM decidir em que dia estamos.

Prefira o construtor a um setter ou a um Clock estático global. O objeto já nasce com o relógio que vai usar, o teste passa o relógio que quiser e não sobra nenhum now() escondido. Se a decisão de vencimento for registrada em log, inclua o instante lido do Clock junto com o resultado. Assim, quando alguém perguntar por que a fatura foi considerada atrasada, a resposta já está no log.

A DevDojo adota esse padrão em qualquer classe de domínio que decide algo com base no "agora". Se a classe não consulta a hora atual, ela não precisa receber um Clock.

Onde o Clock não alcança

Tem quem imagine que Clock.fixed congela a JVM inteira. Ele não congela. É um objeto comum, e só o código que recebe esse objeto passa a usar a hora dele. O teste clockFixedDoesNotFreezeSystemCurrentTimeMillisOrInstantNowNoArg verifica isso:

@Test
void clockFixedDoesNotFreezeSystemCurrentTimeMillisOrInstantNowNoArg() {
    Clock fixed = Clock.fixed(Instant.parse("2026-10-26T08:00:00Z"), ZoneOffset.UTC);
    ClockInvoice invoice = new ClockInvoice(DUE_INSTANT, DUE_DATE, fixed);

    assertEquals(fixed.instant(), Instant.now(fixed));            // 08:00Z
    assertNotEquals(fixed.instant(), invoice.leakWallInstant());  // Instant.now() sem argumento
    assertNotEquals(fixed.millis(), invoice.leakCurrentTimeMillis()); // System.currentTimeMillis()
}

O que fica de fora:

  • System.currentTimeMillis() e Instant.now() sem argumento continuam lendo o relógio da máquina, mesmo com um Clock.fixed no mesmo objeto. Um único now() esquecido num método auxiliar faz o teste voltar a depender da hora em que roda.
  • Bibliotecas que não recebem o Clock veem o horário real. Se uma dependência calcula expiração chamando Instant.now() internamente, seu Clock.fixed não interfere nela.
  • O ZoneId do Clock muda LocalDate.now(clock), mas não muda Instant.now(clock). Trocar o fuso do relógio fixo não altera a regra por instante, só a regra por data.
  • mockStatic(Instant.class) no Mockito parece um atalho, mas não resolve. O código de produção continua com now() fixo no código, e o javadoc de Clock propõe justamente obter o tempo de um objeto em vez de um método estático.

Se o teste precisa simular "daqui a uma hora", existe Clock.offset(base, Duration). Na suíte, Clock.offset(fixed, Duration.ofHours(1)) devolve 09:00Z, e só para quem recebe esse relógio.

Próximo passo

Procure Instant.now() e LocalDate.now() sem argumento no código de domínio que decide vencimento, expiração ou prazo:

grep -rnE 'Instant\.now\(\)|LocalDate\.now\(\)' src/main/java

Para cada ocorrência que decide um boolean de vencimento, adicione um Clock ao construtor, troque a chamada por Instant.now(clock) ou LocalDate.now(clock) e escreva um teste com Clock.fixed de cada lado da fronteira: um minuto antes e um minuto depois do vencimento, e, na regra por data, a mesma hora em dois fusos. Esse teste dá o mesmo resultado às 07h e depois do almoço.

javatesting

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

Conhecimento só conta quando vira prática.

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

Explorar mais artigos