Spring Boot - Estratégias para testar Rest API
Tags: spring, java, testing, beginners
Published: 2021-06-03 20:28:57

# 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:

```java
@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)`

```java
@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`).

```java
@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.

```java
@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](https://github.com/wesleyegberto/spring-study/tree/master/spring-tests-rest-controller-strategies).