Skip to main content

Spring Test Best Practices

Welcome to our guide on Spring test best practices! After learning the fundamentals of testing Spring applications, it's important to understand the best practices that will help you write more effective, maintainable tests. This guide will walk you through key strategies to improve your Spring testing approach.

Introduction

Testing is a crucial aspect of software development, ensuring your Spring applications function correctly and remain maintainable. However, simply knowing how to write tests isn't enough—following established best practices will make your tests more valuable, reliable, and easier to maintain.

In this guide, we'll explore proven strategies for organizing tests, managing test scope, implementing mocks effectively, and optimizing test performance in Spring applications.

Organizing Your Tests

Package Structure

Your test package structure should mirror your main application structure. This makes it easier to locate tests for specific components.

src/
├── main/java/com/example/app/
│ ├── controller/
│ ├── service/
│ └── repository/
└── test/java/com/example/app/
├── controller/
├── service/
└── repository/

Naming Conventions

Consistent naming helps other developers understand what your tests do:

  • Class names should end with Test: UserServiceTest
  • Method names should clearly describe what they're testing: findUserById_whenUserExists_returnsUser
java
class UserServiceTest {
@Test
void findUserById_whenUserExists_returnsUser() {
// Test implementation
}

@Test
void findUserById_whenUserDoesNotExist_throwsException() {
// Test implementation
}
}

Test Scope Best Practices

Keep Unit Tests Focused

Unit tests should test a single unit of functionality in isolation:

java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;

@InjectMocks
private UserService userService;

@Test
void validateUsername_withValidUsername_returnsTrue() {
// Arrange
String validUsername = "johndoe";

// Act
boolean result = userService.validateUsername(validUsername);

// Assert
assertTrue(result);

// No repository calls should happen in this test!
verifyNoInteractions(userRepository);
}
}

Integration Tests for Component Interaction

Use integration tests to verify that components work together correctly:

java
@SpringBootTest
class UserRegistrationIntegrationTest {
@Autowired
private UserService userService;

@Autowired
private UserRepository userRepository;

@Test
void registerUser_savesToDatabase() {
// Arrange
User newUser = new User("johndoe", "John", "Doe", "[email protected]");

// Act
User savedUser = userService.registerUser(newUser);

// Assert
assertNotNull(savedUser.getId());

// Verify the user was actually saved in the database
Optional<User> retrievedUser = userRepository.findById(savedUser.getId());
assertTrue(retrievedUser.isPresent());
assertEquals("johndoe", retrievedUser.get().getUsername());
}
}

Effective Mocking Strategies

Mock External Dependencies

Always mock dependencies that have external interactions (databases, APIs, etc.):

java
@ExtendWith(MockitoExtension.class)
class WeatherServiceTest {
@Mock
private WeatherApiClient weatherApiClient;

@InjectMocks
private WeatherService weatherService;

@Test
void getCurrentTemperature_returnsFormattedTemperature() {
// Arrange
WeatherData mockWeatherData = new WeatherData();
mockWeatherData.setTemperature(25.5);
when(weatherApiClient.getCurrentWeatherData("London"))
.thenReturn(mockWeatherData);

// Act
String temperature = weatherService.getCurrentTemperature("London");

// Assert
assertEquals("26°C", temperature);
}
}

Use @MockBean for Spring Context Tests

When testing with @SpringBootTest, use @MockBean to replace beans with mocks:

java
@SpringBootTest
class WeatherControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;

@MockBean
private WeatherService weatherService;

@Test
void getWeather_returnsWeatherInfo() throws Exception {
// Arrange
when(weatherService.getCurrentTemperature("London"))
.thenReturn("26°C");

// Act & Assert
mockMvc.perform(get("/api/weather?city=London"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("26°C")));
}
}

Test Data Management

Use Test Fixtures

Create reusable test fixtures to initialize test data:

java
class UserTestFixtures {
public static User createValidUser() {
return User.builder()
.id(1L)
.username("testuser")
.email("[email protected]")
.firstName("Test")
.lastName("User")
.build();
}
}

// In your test
@Test
void validateUser_withValidUser_returnsTrue() {
User validUser = UserTestFixtures.createValidUser();
assertTrue(userValidator.isValid(validUser));
}

Use @TestConfiguration for Test-specific Beans

Create beans specifically for testing with @TestConfiguration:

java
@TestConfiguration
public class TestConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("test-data.sql")
.build();
}
}

@SpringBootTest
@Import(TestConfig.class)
class UserRepositoryIntegrationTest {
// Test implementation
}

Testing Database Operations

Use Test Containers for Database Tests

For realistic database testing, use TestContainers:

java
@SpringBootTest
@Testcontainers
class UserRepositoryPostgresTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");

@DynamicPropertySource
static void configureProperties(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 UserRepository userRepository;

@Test
void findByEmail_returnsUser() {
// Test implementation
}
}

Use @DataJpaTest for Repository Tests

Use @DataJpaTest for efficient repository testing:

java
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;

@Test
void findByUsername_whenUserExists_returnsUser() {
// Arrange
User user = new User("testuser", "Test", "User", "[email protected]");
userRepository.save(user);

// Act
Optional<User> result = userRepository.findByUsername("testuser");

// Assert
assertTrue(result.isPresent());
assertEquals("[email protected]", result.get().getEmail());
}
}

Testing Web Layer Components

Use WebMvcTest for Controller Tests

Test controllers in isolation with @WebMvcTest:

java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void getUserById_whenUserExists_returnsUser() throws Exception {
// Arrange
User mockUser = new User("testuser", "Test", "User", "[email protected]");
mockUser.setId(1L);
when(userService.findById(1L)).thenReturn(Optional.of(mockUser));

// Act & Assert
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
}

Testing REST Controllers

For REST controllers, test request/response serialization:

java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void createUser_withValidData_returnsCreated() throws Exception {
// Arrange
UserDto userDto = new UserDto("newuser", "New", "User", "[email protected]");
User savedUser = new User("newuser", "New", "User", "[email protected]");
savedUser.setId(1L);

when(userService.createUser(any(User.class))).thenReturn(savedUser);

// Act & Assert
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"newuser\",\"firstName\":\"New\",\"lastName\":\"User\",\"email\":\"[email protected]\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/users/1")));
}
}

Test Performance Optimization

Use TestExecutionListeners for Setup/Cleanup

Implement custom TestExecutionListener for complex setup/cleanup operations:

java
public class ResetDatabaseTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void beforeTestMethod(TestContext testContext) {
DataSource dataSource = testContext.getApplicationContext().getBean(DataSource.class);
// Execute cleanup scripts
try (Connection conn = dataSource.getConnection()) {
ScriptUtils.executeSqlScript(conn, new ClassPathResource("cleanup.sql"));
ScriptUtils.executeSqlScript(conn, new ClassPathResource("test-data.sql"));
} catch (SQLException e) {
throw new RuntimeException("Failed to reset database", e);
}
}
}

@SpringBootTest
@TestExecutionListeners(
mergeMode = MergeMode.MERGE_WITH_DEFAULTS,
listeners = ResetDatabaseTestExecutionListener.class
)
class DatabaseIntegrationTest {
// Test methods
}

Use @DirtiesContext Strategically

Apply @DirtiesContext only when absolutely necessary, as it's expensive:

java
@SpringBootTest
class ConfigurationTest {
@Autowired
private ApplicationContext context;

@Test
@DirtiesContext
void modifyingBeans_shouldRecreateContext() {
// This test modifies beans, so we need a fresh context afterwards
}

@Test
void readOnlyTest_doesNotNeedNewContext() {
// This test doesn't modify the Spring context
}
}

Testing Async Operations

Test Async Methods with Awaitility

Use Awaitility to test asynchronous code:

java
@SpringBootTest
class NotificationServiceTest {
@Autowired
private NotificationService notificationService;

@Autowired
private NotificationRepository notificationRepository;

@Test
void sendAsyncNotification_storesNotification() {
// Arrange
String recipient = "[email protected]";
String message = "Test notification";

// Act
notificationService.sendAsyncNotification(recipient, message);

// Assert with Awaitility
await().atMost(5, TimeUnit.SECONDS)
.until(() -> notificationRepository.findByRecipient(recipient).size() > 0);

List<Notification> notifications = notificationRepository.findByRecipient(recipient);
assertEquals(1, notifications.size());
assertEquals(message, notifications.get(0).getMessage());
}
}

Summary

Following these best practices will help you create more effective, maintainable Spring tests:

  1. Organization: Structure tests like your main code and use clear naming conventions
  2. Scope Control: Keep unit tests focused, use integration tests for component interaction
  3. Effective Mocking: Mock external dependencies and use appropriate Spring testing annotations
  4. Data Management: Create reusable test fixtures and use proper test configurations
  5. Database Testing: Use TestContainers and specialized annotations like @DataJpaTest
  6. Web Testing: Use @WebMvcTest for controllers and test request/response handling thoroughly
  7. Performance Optimization: Use execution listeners and apply @DirtiesContext strategically
  8. Async Testing: Use Awaitility for testing asynchronous operations

By applying these best practices, you'll write tests that are more reliable, maintainable, and valuable for your Spring applications.

Additional Resources

Practice Exercises

  1. Refactoring Challenge: Take an existing test class and refactor it according to the naming conventions and organizational principles discussed in this guide.

  2. Mock Strategy Exercise: Write a test for a service that interacts with an external API, using proper mocking techniques.

  3. Integration Test Practice: Create an integration test for a user registration flow that validates inputs, stores data in the database, and sends a confirmation email.

  4. Performance Optimization: Identify tests in your codebase that use @DirtiesContext and evaluate if they actually need it.

  5. Test Container Implementation: Convert a database test that uses an embedded database to use TestContainers instead.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)