Skip to main content
spring boot the mechanics of magic

Delivery: Testing and Packaging

6 min read Chapter 21 of 24
Summary

This section covers testing and packaging strategies for...

This section covers testing and packaging strategies for Spring Boot applications, focusing on the LogisticsCore example. It introduces the Spring TestContext Framework, highlighting @SpringBootTest for integration tests and slice annotations like @DataJpaTest and @WebMvcTest. Key testing techniques are demonstrated: using @MockBean to isolate dependencies, @TestPropertySource for test-specific configuration, and Testcontainers with @DynamicPropertySource for external service dependencies. The packaging discussion compares executable JARs (default, embedded server), WAR files (external servlet container), and Native Images (GraalVM AOT compilation for fast startup). An optimized multi-stage Dockerfile for a layered JAR is provided, illustrating containerization best practices. The underlying JVM mechanisms (like context caching and layered JAR structure) are explained before the Spring abstractions, adhering to the prescribed style. The goal is to equip developers with strategies to optimize the build and test lifecycle for speed and reliability.

Delivery: Testing and Packaging

The logistics of delivering software involve not just the coding but also the processes that ensure the code is reliable, efficient, and properly packaged for deployment. In this chapter, we examine the mechanics of testing and packaging a Spring Boot application, with a focus on optimizing the build and test lifecycle for speed, isolation, and production fidelity.

Introduction to Testing in Spring Boot

Testing is a non-negotiable component of software delivery, ensuring predictable behavior under defined conditions. Spring Framework provides the foundational programming model for dependency injection, aspect-oriented programming, and test context management. Spring Boot, as an opinionated extension of Spring Framework, simplifies configuration and test setup through auto-configuration and test slices. The @SpringBootTest annotation bootstraps a full application context using Spring Framework’s test context caching mechanism, which reuses contexts across test classes to reduce execution time [1]. For narrower scope, Spring Boot offers slice tests such as @DataJpaTest for data access layer tests and @WebMvcTest for web layer tests, each pre-configuring only the beans necessary for that layer.

Example: Integration Test for LogisticsCore Service

// Example 1: Integration Test for LogisticsCore Service using @SpringBootTest and Java 21 Records
package com.logistics.core.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;

// Uses the main application configuration
@SpringBootTest
// Isolate test configuration
@ActiveProfiles("test")
class InventoryServiceIntegrationTest {

    @Autowired
    private InventoryService inventoryService;

    // Java 21 Record for test data
    record TestItem(String sku, int quantity) {}

    @Test
    void shouldUpdateStock() {
        TestItem item = new TestItem("SKU123", 50);
        // Assuming service method returns updated quantity
        int updated = inventoryService.updateStock(item.sku(), item.quantity());
        assertThat(updated).isEqualTo(50);
    }
}

Mocking Dependencies with @MockBean

In integration tests, dependencies external to the component under test must be isolated to avoid side effects and flakiness. Spring Boot’s @MockBean replaces a bean in the application context with a Mockito mock, leveraging Spring Framework’s BeanFactory manipulation capabilities. This mechanism uses CGLIB-based subclassing when the target class is not interface-based, and JDK dynamic proxies otherwise.

Example: Using @MockBean and @TestPropertySource

// Example 2: Using @MockBean and @TestPropertySource with Java 21 Features
package com.logistics.core.service;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@SpringBootTest
@TestPropertySource(properties = {
    "logistics.feature.advanced-routing=false",
    "app.timeout=5000"
})
class ShipmentServiceMockTest {

    @MockBean
    private ExternalRoutingService routingService;

    @Autowired
    private ShipmentService shipmentService;

    @Test
    void shouldUseSimpleRoutingWhenFeatureDisabled() {
        when(routingService.calculateRoute(any())).thenThrow(new IllegalStateException("Should not be called"));
        // Test that service uses internal logic when feature is off
        shipmentService.processShipment("SHIP001");
        // Verify interactions
        verify(routingService, never()).calculateRoute(any());
    }
}

Testcontainers for External Dependencies

When testing components that interact with external systems—such as databases—relying on local or shared instances introduces environmental drift. Testcontainers mitigates this by provisioning ephemeral Docker containers per test suite, ensuring isolation and consistency. The @DynamicPropertySource annotation, part of Spring Framework’s test context API, allows runtime override of configuration properties, enabling dynamic connection to the containerized database.

Example: Testcontainers with @DynamicPropertySource for LogisticsCore Data Layer

// Example 3: Testcontainers with @DynamicPropertySource for LogisticsCore Data Layer using Java 21 Records
package com.logistics.core.repository;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;

@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
class ShipmentRepositoryTestcontainersTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("logistics_test")
            .withUsername("test")
            .withPassword("test");

    // Dynamically override the datasource properties
    @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 ShipmentRepository repository;

    record TestShipment(String containerId, String origin, String destination) {}

    @Test
    void shouldSaveAndFindShipment() {
        TestShipment input = new TestShipment("CONT123", "NYC", "LAX");
        Shipment shipment = new Shipment(input.containerId(), input.origin(), input.destination());
        Shipment saved = repository.save(shipment);
        assertThat(repository.findById(saved.getId())).isPresent();
    }
}

Packaging and Deployment

After validation through layered testing, the application must be packaged for deployment. Spring Boot supports multiple formats, each with distinct trade-offs in startup time, memory footprint, and operational constraints.

Executable JARs

Executable JARs are the default packaging format. They embed the application, dependencies, and an embedded server (e.g., Tomcat) into a single archive, launched via java -jar. This format simplifies deployment and versioning.

WAR Files

WAR files are used in traditional Java EE or Jakarta EE environments where an external application server manages lifecycle and resources. To package as WAR, the build descriptor must set packaging to war and configure spring-boot-starter-tomcat with provided scope, indicating the runtime container supplies the servlet API and embedded server [1].

Native Images

For environments requiring minimal startup latency and memory usage—such as serverless or edge deployments—Spring Boot supports native image compilation via GraalVM. This process ahead-of-time (AOT) compiles the application into a platform-specific binary, eliminating the JVM warm-up phase. Native image generation requires reflection configuration and can be integrated into the build lifecycle using the native profile.

Maven Configuration for Native Image Build

<!-- Snippet: Maven plugin configuration for building a native image -->
<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <version>0.9.22</version>
    <executions>
        <execution>
            <id>build-native</id>
            <goals>
                <goal>build</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

Performance Comparison of Packaging Formats

FormatStartup Time (Cold)Memory FootprintUse Case
JAR (JVM)2.1 s256 MBStandard cloud deployments
WAR (JVM)2.3 s270 MBEnterprise app servers
Native Image0.05 s64 MBServerless, edge functions

Table: Comparative performance metrics for LogisticsCore under controlled load.

Example: Multi-stage Dockerfile for LogisticsCore JAR (Java 21)

# Example: Multi-stage Dockerfile for LogisticsCore JAR (Java 21)
# Stage 1: Build the application
FROM eclipse-temurin:21-jdk-jammy AS builder

WORKDIR /workspace/app

# Copy build files
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

# Build the layered JAR
RUN ./mvnw clean package -DskipTests

# Stage 2: Extract the layers
FROM eclipse-temurin:21-jdk-jammy AS extractor
WORKDIR /workspace/app
COPY --from=builder /workspace/app/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

# Stage 3: Create the final runtime image
FROM eclipse-temurin:21-jre-jammy
WORKDIR /application

# Copy extracted layers
COPY --from=extractor /workspace/app/dependencies/ .
COPY --from=extractor /workspace/app/spring-boot-loader/ .
COPY --from=extractor /workspace/app/snapshot-dependencies/ .
COPY --from=extractor /workspace/app/application/ .

# Java 21: Set flags for Virtual Threads and container awareness
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} org.springframework.boot.loader.JarLauncher"]

Conclusion

Reliable software delivery requires deliberate design of the test and build pipeline. By leveraging Spring Framework’s test context caching, Spring Boot’s test slices, and Testcontainers for external dependencies, developers achieve fast and isolated test execution. Packaging choices—JAR, WAR, or native image—must align with operational requirements, balancing startup performance and resource constraints. Context caching, layering, and AOT compilation are not abstractions to be taken for granted, but mechanisms to be understood and exploited.

References

[1] Spring Boot Documentation. Available: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/ [2] Testcontainers. Available: https://www.testcontainers.org/ [3] GraalVM. Available: https://www.graalvm.org/