Comprehensive Testing in Spring Boot Applications
Learn how to implement comprehensive testing strategies in Spring Boot applications including unit tests, integration tests, and performance tests
Comprehensive Testing in Spring Boot Applications
This guide covers different testing strategies for Spring Boot applications, including unit testing, integration testing, performance testing, and best practices.
Video Tutorial
Learn more about testing Spring Boot applications in this comprehensive video tutorial:
Prerequisites
<dependencies>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- TestContainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- REST Assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<!-- JMeter -->
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_core</artifactId>
<version>5.6.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Unit Testing
Service Layer Testing
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
void whenGetProduct_thenReturnProduct() {
// Arrange
Product product = new Product(1L, "Test Product", BigDecimal.TEN);
when(productRepository.findById(1L)).thenReturn(Optional.of(product));
// Act
Product result = productService.getProduct(1L);
// Assert
assertNotNull(result);
assertEquals("Test Product", result.getName());
assertEquals(BigDecimal.TEN, result.getPrice());
verify(productRepository).findById(1L);
}
@Test
void whenGetNonExistingProduct_thenThrowException() {
// Arrange
when(productRepository.findById(1L)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(ProductNotFoundException.class, () -> {
productService.getProduct(1L);
});
}
}
Controller Layer Testing
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Autowired
private ObjectMapper objectMapper;
@Test
void whenGetProduct_thenReturn200() throws Exception {
Product product = new Product(1L, "Test Product", BigDecimal.TEN);
when(productService.getProduct(1L)).thenReturn(product);
mockMvc.perform(get("/api/products/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Test Product"))
.andExpect(jsonPath("$.price").value(10));
}
@Test
void whenCreateProduct_thenReturn201() throws Exception {
Product product = new Product(null, "New Product", BigDecimal.TEN);
Product savedProduct = new Product(1L, "New Product", BigDecimal.TEN);
when(productService.createProduct(any(Product.class))).thenReturn(savedProduct);
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(product)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("New Product"));
}
}
Repository Layer Testing
@DataJpaTest
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
void whenSaveProduct_thenReturnSavedProduct() {
// Arrange
Product product = new Product(null, "Test Product", BigDecimal.TEN);
// Act
Product savedProduct = productRepository.save(product);
// Assert
assertNotNull(savedProduct.getId());
assertEquals("Test Product", savedProduct.getName());
}
@Test
void whenFindByName_thenReturnProduct() {
// Arrange
Product product = new Product(null, "Test Product", BigDecimal.TEN);
productRepository.save(product);
// Act
Optional<Product> found = productRepository.findByName("Test Product");
// Assert
assertTrue(found.isPresent());
assertEquals("Test Product", found.get().getName());
}
}
Integration Testing
Full Application Context Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ProductRepository productRepository;
@BeforeEach
void setup() {
productRepository.deleteAll();
}
@Test
void whenCreateProduct_thenProductIsCreated() {
// Arrange
ProductDTO productDTO = new ProductDTO("Test Product", BigDecimal.TEN);
// Act
ResponseEntity<Product> response = restTemplate.postForEntity(
"/api/products",
productDTO,
Product.class);
// Assert
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertNotNull(response.getBody());
assertEquals("Test Product", response.getBody().getName());
// Verify in database
Optional<Product> saved = productRepository.findById(response.getBody().getId());
assertTrue(saved.isPresent());
}
}
TestContainers for Database Testing
@Testcontainers
@SpringBootTest
class DatabaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
"postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void registerPgProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private ProductRepository productRepository;
@Test
void whenSaveProduct_thenProductIsSaved() {
Product product = new Product(null, "Test Product", BigDecimal.TEN);
Product saved = productRepository.save(product);
assertNotNull(saved.getId());
assertEquals("Test Product", saved.getName());
}
}
REST Assured Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApiIntegrationTest {
@LocalServerPort
private int port;
private String baseUrl;
@BeforeEach
void setUp() {
baseUrl = "http://localhost:" + port + "/api";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
void whenGetProduct_thenReturnProduct() {
given()
.contentType(ContentType.JSON)
.when()
.get(baseUrl + "/products/1")
.then()
.statusCode(200)
.body("name", equalTo("Test Product"))
.body("price", equalTo(10.00f));
}
@Test
void whenCreateProduct_thenReturnCreated() {
ProductDTO product = new ProductDTO("New Product", BigDecimal.TEN);
given()
.contentType(ContentType.JSON)
.body(product)
.when()
.post(baseUrl + "/products")
.then()
.statusCode(201)
.body("name", equalTo("New Product"))
.body("id", notNullValue());
}
}
Performance Testing
JMeter Test Plan
@Test
void createJMeterTestPlan() {
TestPlan testPlan = new TestPlan("Product API Test Plan");
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.setName("Product API Users");
threadGroup.setNumThreads(100);
threadGroup.setRampUp(10);
threadGroup.setDuration(60);
HTTPSamplerProxy getProductSampler = new HTTPSamplerProxy();
getProductSampler.setDomain("localhost");
getProductSampler.setPort(8080);
getProductSampler.setPath("/api/products/1");
getProductSampler.setMethod("GET");
ResultCollector resultCollector = new ResultCollector();
resultCollector.setFilename("test-results.jtl");
testPlan.addThread(threadGroup);
threadGroup.addSampler(getProductSampler);
testPlan.addResultCollector(resultCollector);
SaveService.saveTree(testPlan, new FileOutputStream("product-api-test.jmx"));
}
Gatling Performance Test
class ProductSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json")
val scn = scenario("Product API Test")
.exec(http("Get Product")
.get("/api/products/1")
.check(status.is(200)))
.pause(1)
.exec(http("Create Product")
.post("/api/products")
.body(StringBody("""{"name":"Test","price":10.00}"""))
.check(status.is(201)))
setUp(
scn.inject(
rampUsers(100).during(10.seconds)
)
).protocols(httpProtocol)
}
Test Coverage with JaCoCo
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
Mutation Testing with PIT
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.15.3</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
<configuration>
<targetClasses>
<param>com.example.product.*</param>
</targetClasses>
<targetTests>
<param>com.example.product.*</param>
</targetTests>
</configuration>
</plugin>
Contract Testing with Spring Cloud Contract
Contract.make {
description "should return product by id"
request {
method GET()
url "/api/products/1"
}
response {
status 200
headers {
contentType applicationJson()
}
body([
id: 1,
name: "Test Product",
price: 10.00
])
}
}
Best Practices
-
Test Organization
- Follow the AAA pattern (Arrange, Act, Assert)
- Use meaningful test names
- Keep tests independent
- Use appropriate test categories
-
Test Data Management
- Use test data builders
- Clean up test data
- Use appropriate test databases
- Avoid sharing state between tests
-
Test Configuration
- Use test profiles
- Externalize test configuration
- Mock external services
- Use appropriate test scope
-
Performance Testing
- Define clear performance criteria
- Test with realistic data volumes
- Monitor resource usage
- Test under various loads
Common Testing Patterns
- Builder Pattern for Test Data
public class ProductBuilder {
private Long id;
private String name = "Default Product";
private BigDecimal price = BigDecimal.TEN;
public static ProductBuilder aProduct() {
return new ProductBuilder();
}
public ProductBuilder withName(String name) {
this.name = name;
return this;
}
public ProductBuilder withPrice(BigDecimal price) {
this.price = price;
return this;
}
public Product build() {
return new Product(id, name, price);
}
}
- Test Utilities
public class TestUtils {
public static Product createTestProduct() {
return ProductBuilder.aProduct()
.withName("Test Product")
.withPrice(BigDecimal.TEN)
.build();
}
public static void cleanDatabase(JdbcTemplate jdbcTemplate) {
jdbcTemplate.execute("TRUNCATE TABLE products CASCADE");
}
}
Conclusion
Comprehensive testing in Spring Boot applications involves:
- Unit testing all layers
- Integration testing with real dependencies
- Performance testing under load
- Contract testing for microservices
- Proper test organization and best practices
For more Spring Boot topics, check out: