Dica Java: @ParameterizedTest #008
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()) 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 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!

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!