Skip to main navigation Skip to main content Skip to page footer

Spring Boot - Testing

| Java Spring Boot

Testing ensures that various parts of your Spring Boot application work as expected. This involves testing components like controllers, services, repositories, and configurations in a smaller context and in a realistic environment.

Integration Testing and TestRestTemplate

In this overview we will focus on integration testing. The code for this can be found at GitHub:

package de.myrmod.testing.integration;

import de.myrmod.testing.Model.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProductIntegrationTest {

	@Autowired
	private TestRestTemplate restTemplate;

	@Test
	public void testGetAllProducts() {
		ResponseEntity<Product[]> response = restTemplate.getForEntity("/api/products", Product[].class);
		assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
		assertThat(response.getBody()).isNotNull();
	}

	@Test
	public void testCreateProduct() {
		Product newProduct = new Product();
		newProduct.setName("Laptop");

		ResponseEntity<Product> response = restTemplate.postForEntity("/api/products", newProduct, Product.class);
		assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
		assertThat(response.getBody()).isNotNull();
		assertThat(response.getBody().getName()).isEqualTo("Laptop");
	}

	@Test
	public void testGetProductById() {
		int productId = 1;
		ResponseEntity<Product> response = restTemplate.getForEntity("/api/products/{id}", Product.class, productId);
		assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
		assertThat(response.getBody()).isNotNull();
		assertThat(response.getBody().getId()).isEqualTo(productId);
	}

	@Test
	public void testUpdateProduct() {
		int productId = 1;
		Product updatedProduct = new Product();
		updatedProduct.setName("Updated Laptop");

		HttpEntity<Product> request = new HttpEntity<>(updatedProduct);
		ResponseEntity<Void> response = restTemplate.exchange(
			"/api/products/{id}", HttpMethod.PUT, request, Void.class, productId);

		assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();

		// verify the update by fetching the product again
		ResponseEntity<Product> getResponse = restTemplate.getForEntity("/api/products/{id}", Product.class, productId);
		assertThat(getResponse.getBody()).isNotNull();
		assertThat(getResponse.getBody().getName()).isEqualTo("Updated Laptop");
	}

	@Test
	public void testDeleteProduct() {
		int productId = 1;

		restTemplate.delete("/api/products/{id}", productId);

		// Optionally, verify deletion by fetching the product (expecting null or 404)
		ResponseEntity<Product> response = restTemplate.getForEntity("/api/products/{id}", Product.class, productId);
		assertThat(response.getStatusCode().is4xxClientError()).isTrue();
	}

	@Test
	public void testExchange() {
		HttpEntity<Void> request = new HttpEntity<>(null);
		ResponseEntity<Product[]> response = restTemplate.exchange(
			"/api/products", HttpMethod.GET, request, Product[].class);

		assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
		assertThat(response.getBody()).isNotNull();
	}
}

In this test class we have some notable things going on. The annotation @SpringBootTest with its webEnvironment set to RANDOM_PORT starts up the entire application, on a non conflicting port. This makes it possible to do tests in parallel and without interruption of other applications. Using the TestRestTemplate we act as a client and request resources from our application controller. TestRestTemplate is a convenience wrapper around RestTemplate, specifically designed for integration testing in Spring Boot applications. It simplifies interaction with the HTTP layer during tests and integrates seamlessly with Spring's test environment.

Common methods of TestRestTemplate include:

  • getForObject(): Retrieves the response body directly as an object.
  • getForEntity(): Retrieves the full ResponseEntity, including status code and headers.
  • postForObject(): Sends a POST request and retrieves the response body directly as an object.
  • postForEntity(): Sends a POST request and retrieves the ResponseEntity.
  • exchange(): Offers more flexibility for HTTP requests with headers and HTTP methods.
  • delete(): Sends an HTTP DELETE request.

Spring Boot tests are by default transactional, there is no need to add the @Transacrtional annotation. Meaning the changes they make won't be persisted unless specified, which helps to isolate the tests so they don't influence each other. You can disable this behavior using @Commit or @Rollback(false) on a method and using @Transactional(propagation = Propagation.NOT_SUPPORTED) on a class level.

 

Best Practices for Integration Testing

  1. Isolate Tests:
    • Use an in-memory database (like H2) to isolate test data.
    • Clean the database state between tests (e.g., via @Sql or @DirtiesContext).
  2. Mock External Dependencies:
    • Use tools like WireMock for external service calls.
  3. Test Only Public APIs:
    • Focus on testing the application's exposed endpoints or integration points.
  4. Avoid Overlapping with Unit Tests:
    • Ensure integration tests focus on the interaction between components, not individual methods.

 

Slice Testing using MockMvc

Slice testing tests an application layer and its behavior in combination with another, like the service layer. We will slice test the ProductController. We can use MockMvc to test the REST endpoints without starting the full application context. Our goal is to only test the controller, not the repository, not the service. This means we have to make sure that our controller has everything it needs without creating the real parts behind hit. We can mock them using Mockito.when. Mockito.when lets us define the responses of our @MockitoBean, which our controller will use to answer its requests.

package de.myrmod.testing.unit;

import de.myrmod.testing.Controller.ProductController;
import de.myrmod.testing.Model.Product;
import de.myrmod.testing.Service.ProductService;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.util.Arrays;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(ProductController.class)
public class ProductControllerTest {

	@Autowired
	private MockMvc mockMvc;

	// since @MockBean is deprecated and will be removed in Spring Boot 3.6
	@MockitoBean
	private ProductService productService;

	@Test
	public void testGetAllProducts() throws Exception {
		// Arrange
		when(productService.getAllProducts()).thenReturn(Arrays.asList(
			new Product() {{
				setId(1L);
				setName("Product 1");
			}},
			new Product() {{
				setId(2L);
				setName("Product 2");
			}}
		));

		// Act & Assert
		mockMvc.perform(get("/api/products")
				.contentType(MediaType.APPLICATION_JSON))
			.andExpect(status().isOk())
			.andExpect(jsonPath("$.length()").value(2))
			.andExpect(jsonPath("$[0].name").value("Product 1"));
	}

	@Test
	public void testCreateProduct() throws Exception {
		// Arrange
		Product newProduct = new Product();
		newProduct.setName("New Product");

		when(productService.saveProduct(Mockito.any(Product.class)))
			.thenReturn(new Product() {{
				setId(1L);
				setName("New Product");
			}});

		// Act & Assert
		mockMvc.perform(post("/api/products")
				.contentType(MediaType.APPLICATION_JSON)
				.content("{\"name\":\"New Product\"}"))
			.andExpect(status().isOk())
			.andExpect(jsonPath("$.id").value(1))
			.andExpect(jsonPath("$.name").value("New Product"));
	}
}

After having setup the required mocks, we can start the actual testing with mockMvc. First we perfom a request to our endpoint, then we can set the request header followed by checking of the response via .andExpect. The inner parts of the MockMvc method calls are populated by different functions from MockMvcRequestBuilders and MockMvcResultMatchers.

Key Notes on Slice Tests

  • @MockBean:
    • Used to mock dependencies (e.g., ProductRepository in the service test or ProductService in the controller test).
    • Ensures isolated testing of the component under test.
  • MockMvc:
    • Allows testing of REST endpoints in a WebMvcTest environment.
    • Avoids starting the full application context.
  • jsonPath:
    • Used to validate JSON responses.
  • Separation of Concerns:
    • Service tests focus on business logic.
    • Controller tests validate the endpoint’s input, output, and status.

Purpose of Slice Tests

Slice tests aim to test a specific "slice" of the application in isolation. In the case of a controller slice test, the focus is on verifying:

  • The controller's ability to handle HTTP requests.
  • The correctness of request-to-response mappings (e.g., status codes, JSON structure).
  • Proper interaction between the controller and the service layer.

Mocking the ProductService allows the test to:

  • Isolate the controller logic.
  • Verify that the controller is calling the correct service methods with the expected parameters.
  • Focus only on HTTP request handling, without involving the actual service or repository logic.

This isolation makes the test faster, simpler, and focused, which is the main goal of a slice test.

 

Back