DEV Community

EronAlves1996
EronAlves1996

Posted on

Mockando LocalDateTime.now()

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, 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

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:

@SpringBootApplication
public class GreeterApplication {

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

  @Bean
  public Clock clock () {
    return Clock.systemDefaultZone();
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

@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";
  }

}
Enter fullscreen mode Exit fullscreen mode

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:

@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());
  }

  // ...

}
Enter fullscreen mode Exit fullscreen mode

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:

    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);
    }
Enter fullscreen mode Exit fullscreen mode

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

 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"));
  }
Enter fullscreen mode Exit fullscreen mode

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:

@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")));
  }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)