Substituindo a hora do sistema para testes em Java
1. Visão geral
Neste tutorial rápido, vamos nos concentrar emdifferent ways to override the system time for testing.
Às vezes, há uma lógica em torno da data atual em nosso código. Talvez algumas chamadas de função, comonew Date() ouCalendar.getInstance(), que eventualmente irão chamarSystem.CurrentTimeMillis.
Para obter uma introdução ao uso deJava Clock, consulteto this article here. Ou, para o uso de AspectJ,here.
2. Usando o relógio emjava.time
O pacotejava.time emJava 8 includes an abstract class java.time.Clock com o objetivo de permitir que relógios alternativos sejam conectados como e quando necessário. Com isso, podemos conectar nossa própria implementação ou encontrar uma que já foi feita para satisfazer nossas necessidades.
Para cumprir nossos objetivos,the above library includes static methods to yield special implementations. Vamos usar dois deles, que retorna uma implementação imutável, thread-safe e serializável.
O primeiro éfixed. A partir dele,we can obtain a Clock that always returns the same*Instant*, detectando que os testes não dependem do relógio atual.
Para usá-lo, precisamos de umInstante umZoneOffset:
Instant.now(Clock.fixed(
Instant.parse("2018-08-22T10:00:00Z"),
ZoneOffset.UTC))
The second static method is offset. Neste, um relógio envolve outro que o torna o objeto retornado capaz de obter instantes posteriores ou anteriores pela duração especificada.
Em outras palavras,it’s possible to simulate running in the future, in the past, or in any arbitrary point in time:
Clock constantClock = Clock.fixed(ofEpochMilli(0), ZoneId.systemDefault());
// go to the future:
Clock clock = Clock.offset(constantClock, Duration.ofSeconds(10));
// rewind back with a negative value:
clock = Clock.offset(constantClock, Duration.ofSeconds(-5));
// the 0 duration returns to the same clock:
clock = Clock.offset(constClock, Duration.ZERO);
Com a classeDuration, é possível manipular de nanossegundos a dias. Além disso, podemos negar uma duração, o que significa obter uma cópia dessa duração com o comprimento negado.
3. Usando programação orientada a aspectos
Outra maneira de substituir a hora do sistema é por AOP. Com esta abordagem,we’re able to weave the System class to return a predefined value which we can set within our test cases.
Além disso, é possível tecer as classes de aplicativo para redirecionar a chamada paraSystem.currentTimeMillis() ounew Date() para outra classe de utilitário de nossa propriedade.
Uma maneira de implementar isso é através do uso do AspectJ:
public aspect ChangeCallsToCurrentTimeInMillisMethod {
long around():
call(public static native long java.lang.System.currentTimeMillis())
&& within(user.code.base.pckg.*) {
return 0;
}
}
No exemplo acima,we’re catching every call to System.currentTimeMillis() inside a specified package, que neste caso éuser.code.base.pckg., * e retornando zero toda vez que esse evento acontecer.
É neste lugar que podemos declarar nossa própria implementação para obter o tempo desejado em milissegundos.
Uma vantagem de usar o AspectJ é que ele opera no nível do bytecode diretamente, então não precisa do código-fonte original para funcionar.
Por esse motivo, não precisaríamos recompilá-lo.
4. Zombando do métodoInstant.now()
Podemos usar a classeInstant para representar um ponto instantâneo na linha do tempo. Normalmente, podemos usá-lo para registrar registros de data e hora do evento em nosso aplicativo. O métodonow() dessa classe nos permite obter o instante atual do relógio do sistema no fuso horário UTC.
Vamos ver algumas alternativas para mudar seu comportamento quando testamos.
4.1. Sobrecarregandonow() com umClock
Podemos sobrecarregar o métodonow() com uma instância fixaClock. Many of the classes in the java.time package have a now() method that takes a Clock parameter, o que torna esta nossa abordagem preferida:
@Test
public void givenFixedClock_whenNow_thenGetFixedInstant() {
String instantExpected = "2014-12-22T10:15:30Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
Instant instant = Instant.now(clock);
assertThat(instant.toString()).isEqualTo(instantExpected);
}
4.2. UsandoPowerMock
Além disso, se precisarmos modificar o comportamento do métodonow() sem enviar parâmetros, podemos usarPowerMock:
@RunWith(PowerMockRunner.class)
@PrepareForTest({ Instant.class })
public class InstantUnitTest {
@Test
public void givenInstantMock_whenNow_thenGetFixedInstant() {
String instantExpected = "2014-12-22T10:15:30Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
Instant instant = Instant.now(clock);
mockStatic(Instant.class);
when(Instant.now()).thenReturn(instant);
Instant now = Instant.now();
assertThat(now.toString()).isEqualTo(instantExpected);
}
}
4.3. Usando JMockit
Como alternativa, podemos usar a bibliotecaJMockit.
JMockit nos oferece duas maneiras democking a static method. Um está usando a classeMockUp:
@Test
public void givenInstantWithJMock_whenNow_thenGetFixedInstant() {
String instantExpected = "2014-12-21T10:15:30Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
new MockUp() {
@Mock
public Instant now() {
return Instant.now(clock);
}
};
Instant now = Instant.now();
assertThat(now.toString()).isEqualTo(instantExpected);
}
E o outro está usando a classeExpectations:
@Test
public void givenInstantWithExpectations_whenNow_thenGetFixedInstant() {
Clock clock = Clock.fixed(Instant.parse("2014-12-23T10:15:30.00Z"), ZoneId.of("UTC"));
Instant instantExpected = Instant.now(clock);
new Expectations(Instant.class) {
{
Instant.now();
result = instantExpected;
}
};
Instant now = Instant.now();
assertThat(now).isEqualTo(instantExpected);
}
5. Zombando do métodoLocalDateTime.now()
Outra classe útil no pacotejava.time é a classeLocalDateTime. Esta classe representa uma data e hora sem um fuso horário no sistema de calendário ISO-8601. O métodonow() desta classe nos permite obter a data e hora atual do relógio do sistema no fuso horário padrão.
Podemos usar as mesmas alternativas para zombar como vimos antes. Por exemplo, sobrecarregandonow() com umClock fixo:
@Test
public void givenFixedClock_whenNow_thenGetFixedLocalDateTime() {
Clock clock = Clock.fixed(Instant.parse("2014-12-22T10:15:30.00Z"), ZoneId.of("UTC"));
String dateTimeExpected = "2014-12-22T10:15:30";
LocalDateTime dateTime = LocalDateTime.now(clock);
assertThat(dateTime).isEqualTo(dateTimeExpected);
}
6. Conclusão
Neste artigo, exploramos diferentes maneiras de substituir o tempo do sistema para testes. Primeiro, vimos o pacote nativojava.time e sua classeClock. A seguir, vimos como aplicar um aspecto para tecer a classeSystem. Finalmente, vimos diferentes alternativas para simular o métodonow() nas classesInstanteLocalDateTime.
Como sempre, as amostras de código podem ser encontradasover on GitHub.