When we first start working with Spring Boot applications, we often focus on building features and functionality, leaving testing as an afterthought. This approach can lead to frustration, rejected pull requests, and ultimately less productive development cycles. This article explores several crucial aspects of testing Spring Boot applications that would have saved me countless hours had I known them when I began my journey.
Testing shouldn’t be an afterthought but an integral part of the development process. Spring Boot offers excellent testing support, but understanding how to use these tools effectively can be challenging for newcomers. Let’s dive into the essential knowledge that can transform your testing approach and productivity.
The Testing Swiss Army Knife
Every Spring Boot project bootstrapped from start.spring.io comes with a powerful testing toolbox right out of the box: the spring-boot-starter-test
dependency. This starter is automatically included in all Spring Boot projects and provides a comprehensive set of testing libraries that work seamlessly together.
Let’s explore what’s included in this testing Swiss Army knife:
- JUnit 5: The core testing framework for Java, providing annotations, assertions, and a rich extension model. It supports features like dynamic tests, parameterized tests, and parallel test execution.
- Mockito: Java’s standard mocking library that allows us to mock collaborators, stub their behavior, and verify interactions.
- AssertJ: A fluent assertion library that makes test assertions more readable and expressive. It supports writing assertions in a sentence-like structure and creating custom assertions for domain objects.
- Hamcrest: Another assertion library that provides matchers for creating more readable test conditions. It’s occasionally used within Spring Test, particularly with MockMVC for verifications.
- JSONassert: A specialized assertion library for JSON data structures that supports both strict and non-strict comparison modes.
- JSONPath: A library that helps navigate complex JSON structures using expressions, making it easier to extract and verify specific elements.
This opinionated testing toolbox gives us everything we need to start testing effectively without worrying about compatibility issues or dependency versions.
Spring Boot’s dependency management ensures all these libraries work together harmoniously.
Beyond these core libraries, Spring Boot also manages versions for other testing tools like Selenium, Testcontainers, and MockWebServer through its dependency management system. We can easily add these dependencies without specifying versions, and Spring Boot will ensure compatibility.
The @SpringBootTest Pitfall
One of the most common pitfalls when testing Spring Boot applications is overusing the @SpringBootTest
annotation. While this annotation is extremely useful for integration testing, using it indiscriminately can lead to slow and brittle tests.
The @SpringBootTest
annotation loads the entire application context, including all layers of our application – web, service, data, and external dependencies. This makes it perfect for comprehensive integration tests but unnecessary and inefficient for more focused tests.
There are two main variants of @SpringBootTest
:
- Default mode: Starts the application context but doesn’t start the web server. It uses a mock servlet environment for web tests.
- With WebEnvironment.RANDOM_PORT or DEFINED_PORT: Starts the full application context, including the embedded web server on a random or specified port.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class CustomerControllerIntegrationTest { @Autowired private WebTestClient webTestClient; @Autowired private CustomerRepository customerRepository; @Test void testCreateCustomer() { // Test implementation using real HTTP calls } } |
The pitfall occurs when we use @SpringBootTest
for testing isolated components and then try to mock out all surrounding dependencies using @MockBean/@MockitoBean
. This approach creates a unique application context for each test configuration, defeating Spring’s test context caching mechanism and significantly slowing down our test suite.
For testing isolated components, consider using plain unit tests with JUnit and Mockito instead of starting the entire Spring context. If we’re testing the “doors of our car”, we don’t need to “start the engine” by loading the whole application context.
Slicing: Testing Only What You Need
Spring Boot offers a powerful concept called “slicing” that allows us to test specific parts of our application without loading the entire context. These slice tests provide a middle ground between unit tests and full integration tests.
Slicing annotations configure only the beans relevant to a specific layer of our application, making tests faster and more focused. Here are some of the most commonly used slice test annotations:
- @WebMvcTest: Tests the web layer, including controllers, filters, and security configuration.
- @DataJpaTest: Tests JPA repositories and related components.
- @RestClientTest: Tests REST clients.
- @JsonTest: Tests JSON serialization and deserialization.
- @JdbcTest, @DataMongoTest, @DataRedisTest: Tests for specific database technologies.
Let’s look at an example using @WebMvcTest
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@WebMvcTest(CustomerController.class) class CustomerControllerTest { @Autowired private MockMvc mockMvc; @MockBean // @MockitoBean private CustomerService customerService; @Test void testCreateCustomer() throws Exception { // Given CustomerDto newCustomer = new CustomerDto("John", "Doe"); given(customerService.createCustomer(any(CustomerDto.class))) .willReturn(1L); // When/Then mockMvc.perform(post("/api/customers") .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"John\",\"lastName\":\"Doe\"}")) .andExpect(status().isCreated()) .andExpect(header().string("Location", "/api/customers/1")); } } |
In this example, we’re only testing the web layer with MockMVC, which provides HTTP-like semantics without starting a real server. We can test HTTP status codes, headers, and even security configurations without needing the entire application context.
The benefits of using slice tests include:
- Faster test execution
- More focused testing of specific components
- Still testing with Spring’s infrastructure (unlike pure unit tests)
- Ability to test HTTP semantics, validation, and security
We can even create our own slice test annotations for custom components like message listeners. Spring Cloud AWS, for example, provides an @SqsTest
annotation for testing SQS message listeners in isolation.
JUnit 4 vs. JUnit 5 Pitfalls
The final pitfall concerns mixing JUnit 4 and JUnit 5 annotations and extensions in the same project. JUnit 5 is a complete redesign of JUnit 4, and the two versions are not directly compatible.
When developers copy code from StackOverflow or other sources without checking the JUnit version, they might introduce incompatible annotations or extensions. This can lead to confusing test results and debugging sessions.
Here’s a quick cheat sheet for recognizing JUnit 4 vs. JUnit 5 annotations:
JUnit 4 | JUnit 5 |
---|---|
@Before | @BeforeEach |
@After | @AfterEach |
@BeforeClass | @BeforeAll |
@AfterClass | @AfterAll |
@Ignore | @Disabled |
@Category | @Tag |
@RunWith | @ExtendWith |
@Rule | @ExtendWith |
To prevent mixing JUnit versions, consider using the Maven Enforcer Plugin to ban JUnit 4 dependencies in projects that have migrated to JUnit 5. For migrating existing projects, tools like OpenRewrite can automate much of the conversion process.
Summary
Testing Spring Boot applications effectively requires understanding the tools available and knowing when to use them.
The key takeaways from this article are:
- Leverage the built-in testing toolbox: Spring Boot provides a comprehensive set of testing libraries through
spring-boot-starter-test
. Use these tools to your advantage. - Choose the right testing approach: Not every test needs the entire application context. Use unit tests for isolated components, slice tests for specific layers, and full integration tests for end-to-end scenarios.
- Understand context management: Be aware of how Spring’s test context caching works and avoid creating unique contexts for each test.
- Use slice tests when appropriate: Take advantage of Spring Boot’s slice test annotations to test specific layers without loading the entire context.
- Be consistent with JUnit versions: Avoid mixing JUnit 4 and JUnit 5 annotations and extensions in the same project.
By applying these principles, we can create more effective, efficient, and maintainable tests for our Spring Boot applications. Remember, testing is not an afterthought but an essential part of the development process that helps us deliver high-quality software.
You can find my talk about this topic on YouTube for more hands-on examples for each tip.
Joyful testing,
Philip