DEV Community

Cover image for Testando das trincheiras: Usando um "clock" fixo
Hugo Marques
Hugo Marques

Posted on

Testando das trincheiras: Usando um "clock" fixo

Outro curtinho sobre testes. Um dos problemas mais comuns que eu vejo é o uso do tempo variável dentro do código. Como assim? Imagine o seguinte exemplo:

@Component
public class TaskScheduler {

    private static final LocalTime START_OF_WORKING_DAY = LocalTime.of(8, 0);
    private static final LocalTime END_OF_WORKING_DAY = LocalTime.of(22, 0);

    public void scheduleTask(Task task) {
        if (shouldSchedule()) {
            executeTaskNow(task);
        }
    }

    public static boolean shouldSchedule() {
        // Get the current time in the system default time zone
        LocalDateTime now = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
        LocalTime currentTime = now.toLocalTime();

        // Check if the current time is within the working hours
        return !currentTime.isBefore(START_OF_WORKING_DAY) && !currentTime.isAfter(END_OF_WORKING_DAY);
    }
}
Enter fullscreen mode Exit fullscreen mode

Qual o problema com o código acima? Devido ao Instant.now() no meio do seu código, você não consegue testar o seu método! Como sua lógica é não-determinística e depende do tempo, o seu teste vai passar/falhar conforme o horário que o teste é executado.

Como corrigir esse problema?

Uma alternativa bem simples a partir do java 8 é utilizar a classe Clock para injetar sua dependência que controla o tempo.

No nosso exemplo acima, nosso código ficaria:

@Component
public class TaskScheduler {

    private static final LocalTime START_OF_WORKING_DAY = LocalTime.of(8, 0);
    private static final LocalTime END_OF_WORKING_DAY = LocalTime.of(22, 0);

   private final Clock clock;

   @Autowired
   public TaskScheduler(Clock clock) {
     this.clock = clock;
   }

    public void scheduleTask(Task task) {
        if (shouldSchedule()) {
            executeTaskNow(task);
        }
    }

    public static boolean shouldSchedule() {
        // Get the current time in the current clock
        LocalDateTime now = LocalDateTime.ofInstant(clock);
        LocalTime currentTime = now.toLocalTime();

        // Check if the current time is within the working hours
        return !currentTime.isBefore(START_OF_WORKING_DAY) && !currentTime.isAfter(END_OF_WORKING_DAY);
    }
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma, você consegue escrever os testes passando o Clock com o tempo que você deseja.

    @Test
    public void testIsNowWithinWorkingHours_withinHours() {
        // Arrange: set a fixed instant within working hours
        Instant fixedInstant = LocalDateTime.of(2024, 6, 1, 10, 0)
                                            .toInstant(ZoneOffset.UTC);
        Clock fixedClock = Clock.fixed(fixedInstant, ZoneId.systemDefault());

        TaskScheduler t = new TaskScheduler(fixedClock);

        // Act: call the method with the fixed clock
        boolean result = t.shouldSchedule();

        // Assert: should be within working hours
        assertTrue(result, "The time should be within working hours");
    }
Enter fullscreen mode Exit fullscreen mode

Dicas interessantes!

1. Eu uso Spring, como eu crio esse Clock pra ser injetado?

Simples, você pode declarar o seu Clock padrão pro sistema em uma classe de @Configuration.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Clock;

@Configuration
public class AppConfiguration {

    @Bean
    public Clock clock() {
        // Retorna o relógio do sistema na zona padrão do sistema
        return Clock.systemDefaultZone();
    }
}
Enter fullscreen mode Exit fullscreen mode

E aí na sua classe é só fazer o @Autowired no construtor que nem fizemos no nosso exemplo acima.

2. Eu uso o meu construtor default em 50 locais diferentes, eu vou ter que alterar todos esses locais pra injetar o Clock agora?

Nada jovem padawan! Um truque bacana é fazer um overloaded constructor:


@Component
public class TaskScheduler {

    private static final LocalTime START_OF_WORKING_DAY = LocalTime.of(8, 0);
    private static final LocalTime END_OF_WORKING_DAY = LocalTime.of(22, 0);

   private final Clock clock;

   // Essa anotação fala pro nosso Spring da massa usar esse construtor
   @Autowired 
   public TaskScheduler() {
     this.clock = Clock.systemDefaultZone();
   } 

  // Esse construtor aqui a gente pode usar pros testes.
   public TaskScheduler(Clock clock) {
     this.clock = clock;
   }
Enter fullscreen mode Exit fullscreen mode

E pronto! Com os dois construtores, você mantém a classe funcionando onde ela já existia, além de permitir a escrita de testes automatizados de forma simples.

Sumário

  1. Evite o uso de tempo variável no meio do código.
  2. Use injeção de dependências para adicionar o seu relógio.
  3. Use construtores padrões e sobrecarga no construtor para permitir adicionar os testes com o mínimo de refatoramento.

Espero que vocês estejam curtindo essas dicas rápidas sobre testes.

Em breve, vou escrever também meus aprendizados sobre paralelismo!

Happy coding!

Retry later

Top comments (0)

Retry later
Retry later