DEV Community

Wesley Egberto
Wesley Egberto

Posted on • Edited on

6 2

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.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (1)

Collapse
 
fernando_arruda_6d9c7b158 profile image
Fernando Arruda

Parabéns pela abordagem!

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs