Spring Boot - Estratégias para testar Rest API

wesleyegberto

Wesley Egberto

Posted on June 3, 2021

Spring Boot - Estratégias para testar Rest API

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)
                )
                .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

É mais recomendado utilizar MockMVC com extensão @SpringExtension porque é mais controlável para testes de um controller específico.

@SpringBootTest com RestTemplate ou TestRestTemplate (com webserver real)

O Spring inicializa toda a aplicação com um webserver real (tomcat, jetty).
Para os testes utilizamos o RestTemplate ou TestRestTemplate, que nos fornece algumas features a mais para facilitar os testes de integração.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PetsControllerSpringBootTest {
    @MockBean
    private PetsRepository petsRepository;

    @Autowired
    private TestRestTemplate restTemplate;

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

        ResponseEntity<Pet> response = restTemplate.getForEntity("/pets/42", Pet.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().equals(new Pet(42, "Marley", "Wesley")));
    }

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

        ResponseEntity<Pet> response = restTemplate.getForEntity("/pets/42", Pet.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        assertThat(response.getBody()).isNull();
    }

    @Test
    public void should_create_new_pet() throws Exception {
        ResponseEntity<Pet> response = restTemplate.postForEntity("/pets",
                new Pet("Marley", "Wesley"), Pet.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);

        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 {
        ResponseEntity<Pet> response = restTemplate.getForEntity("/pets/42", Pet.class);

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

Nota

Vale notar que nos testes estamos mockando a classe PetsRepository porque queremos testar isoladamente nossa API, aqui queremos testar:

  • serialização das models
  • filters
  • validações na controller
  • response com headers

O projeto de exemplo está no github.

💖 💪 🙅 🚩
wesleyegberto
Wesley Egberto

Posted on June 3, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related