Essa é mais uma dica de Testes Unitários!
Com mais algumas dicas ~escondidas~!
Vamos a um exemplo simples de um endpoint POST com body e algumas validações:
@RestController
@RequestMapping("v1/persons")
public class PersonController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void create(@RequestBody @Valid final PersonBody body) {
//impl
}
@With
@Builder
public record PersonBody(@NotBlank @Size(max = 10) String name,
@NotNull @Past LocalDate birthdate) {
}
}
Os testes básicos válidos para esse endpoint são:
- o de sucesso, com todos os campos válidos
- os de erro, com todas as possibilidades de campos inválidos
Em uma contagem rápida, seria necessário UM de sucesso (CREATED) e cerca de SETE de erro (BAD_REQUEST).
O de sucesso é simples:
@WebMvcTest(PersonController.class)
class PersonControllerTest {
private static final String URL = "/v1/persons";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Nested
class WhenPost {
private static PersonBody validBody;
static {
validBody = PersonBody.builder()
.name("Igor")
.birthdate(LocalDate.now().minusDays(1))
.build();
}
@Test
@SneakyThrows
void shouldReturnCreated() {
mockMvc.perform(
post(URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(validBody))
).andExpect(status().isCreated());
}
}
}
Se desejar testar o body/contrato esperado também é possível:
.andExpect(jsonPath("$.").value(<expected>))
Porém o foco aqui são os testes de Bad Request (400).
Seria necessário criar UM método para cada @Test e possibilidade de erro do payload enviado:
@Test
@SneakyThrows
void shouldReturnBadRequestBecauseNameIsNull() {
mockMvc.perform(
post(URL)
.contentType(MediaType.APPLICATION_JSON)
.content(
objectMapper.writeValueAsString(validBody.withName(null))
)
).andExpect(status().isBadRequest());
}
Se tornaria maçante criar as SETE possibilidades de erros (isso que é um body simples, com apenas 2 campos, imagine payloads mais complexos).
E aqui vai uma solução muito interessante: @ParameterizedTest !
@ParameterizedTest
@MethodSource("badBodies")
@SneakyThrows
void shouldReturnBadRequest(final PersonBody body) {
mockMvc.perform(
post(URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body))
).andExpect(status().isBadRequest());
}
static Stream<Arguments> badBodies() {
return Stream.of(
Arguments.of(validBody.withName(null)),
Arguments.of(validBody.withName("")),
Arguments.of(validBody.withName(" ")),
Arguments.of(validBody.withName("112233445566")),
Arguments.of(validBody.withBirthdate(null)),
Arguments.of(validBody.withBirthdate(LocalDate.now())),
Arguments.of(validBody.withBirthdate(LocalDate.now().plusDays(1)))
);
}
}
Para testes com MÚLTIPLAS situações que devem ter o MESMO resultado os testes parametrizados são perfeitos! Para testes em endpoints que possuam validações o @ParameterizedTest serve como uma luva!
Podem notar que utilizei o @MethodSource
para alimentar o argumento do teste, existem outras formas de prover o argumento (pacote: org.junit.jupiter.params.provider
).
"Aaaah, além do 400 eu quero testar a mensagem do erro, se é a correta para aquele campo ou não." Essa dica será em outro post!