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

  1. Test Organization

    • Follow the AAA pattern (Arrange, Act, Assert)
    • Use meaningful test names
    • Keep tests independent
    • Use appropriate test categories
  2. Test Data Management

    • Use test data builders
    • Clean up test data
    • Use appropriate test databases
    • Avoid sharing state between tests
  3. Test Configuration

    • Use test profiles
    • Externalize test configuration
    • Mock external services
    • Use appropriate test scope
  4. Performance Testing

    • Define clear performance criteria
    • Test with realistic data volumes
    • Monitor resource usage
    • Test under various loads

Common Testing Patterns

  1. 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);
    }
}
  1. 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: