Mockando LocalDateTime.now()
Tags: java, testing, webdev, programming
Published: 2023-12-21 14:36:25

Um dos principais aspectos dentro de software são as qualidades das entregas. Não adianta eu construir um software do zero e não garantir que ele esteja funcionando conforme o esperado (ou conforme o documento de especificação). Um dos meios que podemos garantir que nosso software funciona é através de testes, mais popularmente através de Testes Unitários.

Para garantir a validade dos nossos testes unitários, normalmente isolamos o sistema que está sob teste, para garantir que ele irá atingir as especificações dentro de um ambiente controlado. Frequentemente vemos situações de testes que falham em condições específicas. Um exemplo bem rápido acontecia no horário de verão, onde os relógios eram trocados, porém por efeitos do fuso horário, em alguns horários estes testes falhavam.

As questões relativas a horários, no Java, a partir do Java 8, é feita utilizando a API do `LocalDate` e do `LocalDateTime` e muitas vezes precisamos realizar o mock dessas classes, principalmente do método `.now()`. Como fazer isso?

No caso, o principal recurso que temos para isso é nos apoiarmos em cima de injeção de dependências. Se verificarmos no [javadoc da classe LocalDateTime](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html), veremos que o método `.now()` possui um overload que aceita um objeto de `Clock`, bem como outro overload que aceita um `ZoneId`. Podemos utilizar qualquer um dos dois para nossos propósitos:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2xc4ia01kx87iun3fopt.png)

A ideia aqui então é passar este objeto de `Clock` para o método `.now()`, de forma que, ao chamar este método, ele irá refletir o horário que quisermos. 

Para este exemplo, eu utilizei Spring Web para realizar um teste, onde na aplicação, ela exibe um tipo de cumprimento baseado no horário de acesso.

Sendo assim, eu declaro a bean do `Clock` em uma classe de configuração do Spring:

```java
@SpringBootApplication
public class GreeterApplication {

  public static void main (String[] args) {
    SpringApplication.run(GreeterApplication.class, args);
  }

  @Bean
  public Clock clock () {
    return Clock.systemDefaultZone();
  }
}
```

Aqui na bean, eu especifico que o `Clock` terá origem no próprio sistema. Assim, na aplicação real, já vai estar ajustado para o horário real.

Já no Controller, eu injeto esta bean para utilizar no `LocalDateTime` e capturar o horário atual.

```java
@Controller
public class GreeterController {

  private Clock clock;

  public GreeterController (Clock clock) {
    super();
    this.clock = clock;
  }

  @GetMapping("/")
  public String doGreet (Model model) {
    var nowHour = LocalDateTime.now(clock).getHour();

    if (nowHour < 12) model.addAttribute("greet", "Good Morning!!");
    else if (nowHour < 18) model.addAttribute("greet", "Good Afternoon!!");
    else model.addAttribute("greet", "Good Night!!");

    return "greet";
  }

}
```

Feito isso, minha aplicação está perfeita para poder realizar os testes, pois nele eu só declaro ela como uma `@MockBean`, que é uma anotação do Spring para poder injetar beans de mock dentro do Contexto da Aplicação, e assim realizar o teste:

```java
@WebMvcTest(GreeterController.class)
public class GreetControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private Clock clock;

  // ...

  private void setupMocksForClock(Clock mockClock) {
    when(clock.instant()).thenReturn(mockClock.instant());
    when(clock.getZone()).thenReturn(mockClock.getZone());
  }

  // ...

}
```

Observe que utilizo o Mockito aqui para poder manipular quais deverão ser os retornos do meu `Clock` quando forem feitas chamadas a métodos específicos. Por que estes métodos? Lembra que a gente passa esse `Clock` para o `LocalDateTime.now()`?
Se formos olhar a implementação desse overload, observe que ele chama estes dois métodos para definir o horário atual:

```java
    public static LocalDateTime now(Clock clock) {
        Objects.requireNonNull(clock, "clock");
        final Instant now = clock.instant();  // called once
        ZoneOffset offset = clock.getZone().getRules().getOffset(now);
        return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);
    }
```

Então ok, defini mais alguns métodos utilitários para poder me ajudar nestes testes:

```java
 private Clock prepareClock (LocalDateTime time) {
    return Clock
        .fixed(time.toInstant(ZoneOffset.ofHours(-3)), ZoneId.systemDefault());
  }

  private ResultActions doDefaultAssertions ()
      throws Exception {
    return mockMvc.perform(get("/"))
        .andExpect(status().isOk())
        .andExpect(view().name("greet"));
  }
```

Agora posso fazer os testes de acordo com as faixas de horário que defini na aplicação, e testar, de maneira objetiva, que ela funciona de forma correta:

```java
@Test
  public void testGreetMorning ()
      throws Exception {
    Clock fixedClock = prepareClock(LocalDateTime.of(2023, 12, 8, 11, 25, 00));

    setupMocksForClock(fixedClock);

    doDefaultAssertions()
        .andExpect(content().string(containsString("Good Morning")));
  }

  @Test
  public void testGreetAfternoon ()
      throws Exception {
    Clock fixedClock = prepareClock(LocalDateTime.of(2023, 12, 8, 12, 42, 00));

    setupMocksForClock(fixedClock);

    doDefaultAssertions()
        .andExpect(content().string(containsString("Good Afternoon")));
  }

  @Test
  public void testGreetNight ()
      throws Exception {
    var fixedClock = prepareClock(LocalDateTime.of(2023, 12, 8, 18, 05));

    setupMocksForClock(fixedClock);

    doDefaultAssertions()
        .andExpect(content().string(containsString("Good Night")));
  }
```