Spring Boot – Estratégias para testar Rest API

Posted on



Spring Boot – Estratégias para testar Rest API

Para efetuar o teste de uma aplicação Spring Boot com REST API temos dois métodos:

  • Inside-server test:
    • Standalone-mode: usar MockMVC sem contexto
    • Spring context: usar MockMVC gerenciado pelo Spring
  • Outside-server test
    • SpringBootTest com mock: usar MockMVC
    • Integration test: usar RestTemplate ou TestRestTemplate

Independente da forma de configuração do testes, a escrita será similar, variando apenas na forma de mandar o body da requisição onde podemos escrever o JSON puro ou serializar um objeto.



Inside-Server Test



MockMVC com Standalone-mode

Podemos executar o teste em standalone-mode onde o contxto do Spring não é carregado.
Nele mockamos as dependências da controller e instânciamos outros beans necessários manualmente.

  • JUnit 4: utiliza o runner MockitoJUnitRunner
  • JUnit 5: utiliza a extensão MockitoExtension

Usamos a classe MockMvcBuilders para criar o contexto para teste fornecendo todas as peças necessárias:

@ExtendWith(MockitoExtension.class)
public class PetsControllerMockMvcStandaloneTest {
    private MockMvc mvc;

    @Mock
    private PetsRepository petsRepository;

    @InjectMocks
    private PetsController petsController;

    private JacksonTester<Pet> json;

    @BeforeEach
    public void setup() {
        // se estiver usando JUnit 4
        // MockitoAnnotations.initMocks(this);

        // não podemos usar @AutoConfigureJsonTesters (já que não existe o contexto do Spring - então inicializamos na mão)
        JacksonTester.initFields(this, new ObjectMapper());

        MockMvcBuilders.standaloneSetup(petsController)
                .setControllerAdvice(new PetExceptionHandler())
                .addFilters(new ApiVersionFilter())
                .build();
    }

    @Test
    public void should_return_existing_pet() throws Exception {
        given(petsRepository.findById(42))
                .willReturn(Optional.of(new Pet(42, "Marley", "Wesley")));

        MockHttpServletResponse response = mvc.perform(
                    get("/pets/42").accept(MediaType.APPLICATION_JSON)
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString())
            .isEqualTo(
                json.write(new Pet(42, "Marley", "Wesley")).getJson()
            );
    }

    @Test
    public void should_return_not_found_for_non_existing_pet() throws Exception {
        given(petsRepository.findById(42))
                .willThrow(new PetNotFoundException());

        MockHttpServletResponse response = mvc.perform(
                    get("/pets/42").accept(MediaType.APPLICATION_JSON)
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
        assertThat(response.getContentAsString()).isEmpty();
    }

    @Test
    public void should_create_new_pet() throws Exception {
        MockHttpServletResponse response = mvc.perform(
                    post("/pets")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(
                            json.write(new Pet("Marley", "Wesley")).getJson()
                        )
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());

        ArgumentCaptor<Pet> argCaptor = ArgumentCaptor.forClass(Pet.class);
        verify(petsRepository).save(argCaptor.capture());
        Pet pet = argCaptor.getValue();

        assertThat(pet.getId()).isEqualTo(0);
        assertThat(pet.getName()).isEqualTo("Marley");
        assertThat(pet.getOwner()).isEqualTo("Wesley");
    }

    @Test
    public void should_add_api_version_header() throws Exception {
        MockHttpServletResponse response = mvc.perform(
                    get("/pets/42").accept(MediaType.APPLICATION_JSON)
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getHeaders("X-PETS-VERSION")).containsOnly("v1");
    }
}
Enter fullscreen mode

Exit fullscreen mode



MockMVC com Spring Context

Podemos executar o teste inicializando o contexto do Spring.
O runner provido pelo Spring irá carregar todo contexto necessário para o controle (mocks, filters, advices, etc).
Esse formato é mais considerado Integration Test porque outros elementos do Spring e da aplicação (filters, advices) são adicionados automaticamente.

Nota: no Spring Boot 2.1+, as anotações @...Tests do Spring já são decorados com @ExtendWith(SpringExtension.class)

@AutoConfigureJsonTesters
@WebMvcTest(PetsController.class)
public class PetsControllerMockMvcWithContextTest {
    @Autowired
    private MockMvc mvc;

    @MockBean
    private PetsRepository petsRepository;

    // inicializado automaticamente pelo @AutoConfigureJsonTesters
    @Autowired
    private JacksonTester<Pet> json;

    @Test
    public void should_return_existing_pet() throws Exception {
        given(petsRepository.findById(42))
                .willReturn(Optional.of(new Pet(42, "Marley", "Wesley")));

        MockHttpServletResponse response = mvc.perform(
                    get("/pets/42").accept(MediaType.APPLICATION_JSON)
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString())
            .isEqualTo(
                json.write(new Pet(42, "Marley", "Wesley")).getJson()
            );
    }

    @Test
    public void should_return_not_found_for_non_existing_pet() throws Exception {
        given(petsRepository.findById(42))
                .willThrow(new PetNotFoundException());

        MockHttpServletResponse response = mvc.perform(
                    get("/pets/42").accept(MediaType.APPLICATION_JSON)
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
        assertThat(response.getContentAsString()).isEmpty();
    }

    @Test
    public void should_create_new_pet() throws Exception {
        MockHttpServletResponse response = mvc.perform(
                    post("/pets")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(
                            json.write(new Pet("Marley", "Wesley")).getJson()
                        )
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());

        ArgumentCaptor<Pet> argCaptor = ArgumentCaptor.forClass(Pet.class);
        verify(petsRepository).save(argCaptor.capture());
        Pet pet = argCaptor.getValue();

        assertThat(pet.getId()).isEqualTo(0);
        assertThat(pet.getName()).isEqualTo("Marley");
        assertThat(pet.getOwner()).isEqualTo("Wesley");
    }

    @Test
    public void should_add_api_version_header() throws Exception {
        MockHttpServletResponse response = mvc.perform(
                    get("/pets/42").accept(MediaType.APPLICATION_JSON)
                )
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getHeaders("X-PETS-VERSION")).containsOnly("v1");
    }
}
Enter fullscreen mode

Exit fullscreen mode



Outside-Server Test

É utilizado a anotação @SprintBootTest.
Spring inicializa toda a aplicação com todas suas dependências, o que torna o teste mais lento.
Um webserver real pode ou não ser inicializado (dependendo do valor da propriedade webEnvironment da anotação).
É possível ainda utilizar mocks ou desativar alguns componentes.



@SpringBootTest com MockMvc (sem webserver real)

O Spring inicializa toda a aplicação sem um webserver real.

Quando usamos a anotação sem parâmetros ou com webEnvironment = WebEnvironment.MOCK estamos criando um contexto igual ao MockMVC com contexto do Spring (usando extensão @SpringExtension).

@SpringBootTest
@AutoConfigureJsonTesters
@AutoConfigureMockMvc
public class PetsControllerSpringBootMockTest {
@Autowired
private MockMvc mvc;

@MockBean
private PetsRepository petsRepository;

// inicializado automaticamente pelo @AutoConfigureJsonTesters
@Autowired
private JacksonTester<Pet> json;

@Test
public void should_return_existing_pet() throws Exception {
given(petsRepository.findById(42))
.willReturn(Optional.of(new Pet(42, "Marley", "Wesley")));

MockHttpServletResponse response = mvc.perform(
get("/pets/42").accept(MediaType.APPLICATION_JSON

Leave a Reply

Your email address will not be published. Required fields are marked *