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:
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();
}
}
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";
}
}
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());
}
// ...
}
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);
}
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"));
}
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")));
}
Top comments (0)