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!

Apr 28, 2025 - 22:36
 0
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(<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!