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-plugin3.14.0,maven-surefire-plugin3.6.0org.junit.jupiter:junit-jupiter6.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ãoLocalDate.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ógio | Regra | Valor lido | Vencido? |
|---|---|---|---|
Clock.fixed(07:00Z, UTC) | instante vs 08:00Z | Instant 07:00Z | false |
Clock.fixed(09:00Z, UTC) | instante vs 08:00Z | Instant 09:00Z | true |
Clock.fixed(02:00Z, America/Sao_Paulo) | data vs 2026-10-25 | Instant 02:00Z, LocalDate 2026-10-25 | false |
Clock.fixed(03:00Z, America/Sao_Paulo) | data vs 2026-10-25 | LocalDate 2026-10-26 | true |
Clock.fixed(02:00Z, UTC) | data vs 2026-10-25 | Instant 02:00Z, LocalDate 2026-10-26 | true |
Relógio da máquina, ~05:13Z, Etc/UTC | instante vs 08:00Z | Instant.now() | false |
Relógio da máquina, ~05:13Z, Etc/UTC | data vs 2026-10-25 | LocalDate.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 desystemDefaultZone(), quando você precisa do instante atual sem data ou hora. É oClockInvoice.productionUtc(...)acima. Na suíte,productionSystemUtcMatchesWallInstantOnThisHostconfirma que ele fica a menos de 2 s doInstant.now()neste host. - A regra compara datas (venceu no dia 25?): use um
Clockcom oZoneIddo negócio declarado explicitamente, comoClock.system(ZoneId.of("America/Sao_Paulo")), que é oproductionInZone(...). No teste, o equivalente éClock.fixed(instant, zona). EviteClock.systemDefaultZone()eLocalDate.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()eInstant.now()sem argumento continuam lendo o relógio da máquina, mesmo com umClock.fixedno mesmo objeto. Um úniconow()esquecido num método auxiliar faz o teste voltar a depender da hora em que roda.- Bibliotecas que não recebem o
Clockveem o horário real. Se uma dependência calcula expiração chamandoInstant.now()internamente, seuClock.fixednão interfere nela. - O
ZoneIddoClockmudaLocalDate.now(clock), mas não mudaInstant.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 comnow()fixo no código, e o javadoc deClockpropõ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.