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
- Standalone-mode: usar
- Outside-server test
- SpringBootTest com mock: usar
MockMVC
- Integration test: usar
RestTemplate
ouTestRestTemplate
- 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!