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 MockMVCsem contexto
- Spring context: usar MockMVCgerenciado pelo Spring
 
- Standalone-mode: usar 
- Outside-server test
- SpringBootTest com mock: usar MockMVC
- Integration test: usar RestTemplateouTestRestTemplate
 
- SpringBootTest com mock: usar 
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");
    }
}
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");
    }
}
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");
    }
}
É 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");
    }
}
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.
 
 
    
Top comments (1)
Parabéns pela abordagem!