Testing Spring Boot Applications: Best Practices and Frameworks

Rajvi Upadhyay
Simform Engineering
10 min readOct 12, 2023

Mastering Quality Control with Spring Boot

Testing is an integral part of software development. It ensures that your Spring Boot applications work as expected and continue to do so as they evolve. In this article, we’ll explore how to test Spring Boot applications using best practices and tools.

Why Testing Matters

Testing is crucial for several reasons:

  1. Reliability: It ensures your application performs correctly and reliably.
  2. Bug Detection: It helps identify and fix issues early in development.
  3. Refactoring: It allows for code refactoring with confidence so existing functionality won’t break.
  4. Documentation: Well-written tests serve as living documentation for your code.

Types of Testing

Spring Boot applications can be tested at various levels, including:

  1. Unit Testing: Testing individual components, such as classes or methods, in isolation.
  2. Integration Testing: Verifying that different components or services work correctly together.
  3. Functional Testing: Testing the application’s functionality from the user’s perspective.
  4. End-to-End Testing: Testing the entire application, including its external dependencies, in a production-like environment.

Best Practices for Testing Spring Boot Applications

1. Keep Tests Isolated

Ensure that tests are independent of each other. Each test should set up its required context, run it, and tear down any resources it creates. This prevents one test from affecting the outcome of another.

2. Leverage Spring Boot’s Testing Annotations

Spring Boot provides testing annotations like @SpringBootTest, @DataJpaTest, and @WebMvcTest that simplify testing specific parts of your application. Use them to load only the necessary parts of your application context for more efficient testing. Below are some examples.

a. Use @SpringBootTest to load the entire Spring context.

@SpringBootTest is an annotation used to load the entire Spring context for your tests. This is useful while testing multiple components in an application together. Use the @Test annotation to mark test methods.

Here is an example that explains using Mockito with JUnit.

<!-- Adding JUnit 5(jupiter) and mockito dependencies to our pom.xml -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import com.employee.service.EmployeeService;
import com.employee.dao.EmployeeRepository;
import com.employee.dto.Employee;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTests {

@InjectMocks
private EmployeeService employeeService;

@Mock
private EmployeeRepository employeeRepository;

@Test
public void testGetEmployeeById() {
// Arrange
long employeeId = 1L;
Employee mockEmployee = new Employee(employeeId, "John Doe", "john.doe@example.com");

// Mock the behavior of the repository to return the mock employee
Mockito.when(employeeRepository.findById(employeeId)).thenReturn(Optional.of(mockEmployee));

// Act
Employee result = employeeService.getEmployeeById(employeeId);

// Assert
assertNotNull(result);
assertEquals(employeeId, result.getId());
assertEquals("John Doe", result.getName());
assertEquals("john.doe@example.com", result.getEmail());
}
}

b. Use MockMvc to simulate HTTP requests and test the response from your controller. Use @AutoConfigureMockMvc to automatically configure MockMvc.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@SpringBootTest
@AutoConfigureMockMvc
public class GreetingControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
public void testGreetEndpoint() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/greet"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}

c. Use @MockBean to replace a real bean with a mock implementation.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import com.user.service.UserService;
import com.user.dao.UserRepository;
import com.user.dto.User;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class UserServiceTest {

@Autowired
private UserService userService;

@MockBean
private UserRepository userRepository;

@Test
public void testFindUserByUsername() {
// Define a sample user
User sampleUser = new User();
sampleUser.setId(1L);
sampleUser.setUsername("john_doe");
sampleUser.setEmail("john@example.com");

// Mock the repository behavior
when(userRepository.findByUsername("john_doe")).thenReturn(sampleUser);

// Perform the test
User foundUser = userService.findUserByUsername("john_doe");

// Assertions
assertThat(foundUser).isNotNull();
assertThat(foundUser.getUsername()).isEqualTo("john_doe");
assertThat(foundUser.getEmail()).isEqualTo("john@example.com");
}
}

@Mock vs @MockBean:

@Mock is used for unit testing and mocking objects outside of the Spring context, while @MockBean is used for Spring Boot integration testing to replace real beans with mock or spy versions within the Spring application context.

d. Use @DataJpaTest to test repositories with an embedded database.

If you’re using JPA in your Spring Boot application, @DataJpaTest loads only the components relevant to JPA testing, such as entity classes, repository interfaces, and the necessary Spring Data JPA configuration.

By default, Spring Boot will create a transaction for each test method when using @DataJpaTestand roll back the transactions at the end of each test method.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import com.user.dao.UserRepository;
import com.user.dto.User;
import static org.junit.Assert.*;

@DataJpaTest
public class UserRepositoryTest {

@Autowired
private UserRepository userRepository;

@Test
public void testGetAllUsers() {
User user = userRepository.updateUserById(1,"Tom");
assertNotNull(user);
}
}

e. Use @AutoConfigureTestDatabase to configure the test database.

When using @DataJpaTest, Spring Boot will automatically configure an in-memory H2 database for your tests, making it easy to test your database queries without a separate database instance. However, if you need a different database, you can use the @AutoConfigureTestDatabase annotation to configure it.

import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import com.user.dao.UserRepository;
import com.user.dto.User;
import static org.junit.Assert.*;

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class UserRepositoryTest {

@Autowired
private UserRepository userRepository;

@Test
public void testGetUsersNotNull() {
List<User> userList = userRepository.getUsers();

assertNotNull(userList);
}
}

@AutoConfigureTestDatabase(replace = Replace.NONE) configures the test database behavior. In this case, it replaces nothing and instead uses the default database configuration specified in your application.properties or application.yml.

f. Use @BeforeEach and @AfterEach to set up and tear down test fixtures.

These annotations allow you to define methods that will run before and after each test method.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import com.employee.service.EmployeeService;
import static org.junit.Assert.*;

@SpringBootTest
public class EmployeeServiceTest {

private EmployeeService employeeService;

@BeforeEach
public void setUp() {
// Initialize the EmployeeService or set up resources if needed
employeeService = new EmployeeService();
}

@AfterEach
public void tearDown() {
// Clean up resources or perform other cleanup tasks
employeeService = null;
}

@Test
public void testGenerateWelcomeMessage() {
String name = "John";
String welcomeMessage = employeeService.generateWelcomeMessage(name);

assertEquals("Welcome, John!", welcomeMessage);
}
}

3. Test Configuration

Spring Boot allows you to configure different application properties for different profiles (e.g., application.properties, application-test.properties). It allows you to create custom test configurations without affecting the main application context.

a. Use @TestConfiguration to provide additional beans for testing.

@SpringBootTest will bootstrap the full application context, which means you can autowire any bean that’s picked up by component scanning into our test. You might want to avoid bootstrapping the real application context but use a special test configuration. You can achieve this with @TestConfiguration annotation. There are two ways of using the annotation.

i. Create a static inner class in the same test class where we want to autowire the bean.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.Assert.*;
import com.employee.service.EmployeeService;

@SpringBootTest
public class EmployeeServiceTest {

@TestConfiguration
public static class TestEmployeeServiceConfig {
@Bean
public EmployeeService employeeService() {
return new EmployeeService();
}
}

@Autowired
private EmployeeService employeeService;

@Test
public void testWelcomeMessage() {
String message = employeeService.getWelcomeMessage();

assertEquals("Welcome", message);
}
}

ii. Create a separate test configuration class and import it using the @Import annotation.

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import com.employee.service.EmployeeService;

@TestConfiguration
public class TestEmployeeServiceConfig {

@Bean
public EmployeeService employeeService() {
return new EmployeeService();
}
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import com.employee.service.EmployeeService;
import static org.junit.Assert.*;

@SpringBootTest
@Import(TestEmployeeServiceConfig.class)
public class EmployeeServiceTest {

@Autowired
private EmployeeService employeeService;

@Test
public void testWelcomeMessage() {
String message = employeeService.getWelcomeMessage();

assertEquals("Simform Welcomes You", message);
}
}

b. Use @ConfigurationProperties to inject properties from a configuration file.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "app")
@ConfigurationPropertiesScan
public class AppProperties {

private String appName;

private String appVersion;

// Getters and setters
}
#Add the configuration properties to your application's property file
app.appName=Keka
app.appVersion=5.0

Spring will automatically bind any property defined in our property file with the prefix app and the same name as one of the fields in the AppProperties class. You can then use these properties in your test beans by autowiring the AppProperties bean.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.app.service.ApplicationService;
import com.app.properties.AppProperties;
import com.app.dto.Application;
import static org.junit.Assert.*;

@SpringBootTest
public class ApplicationServiceTest {

@Autowired
private AppProperties properties;

@Autowired
private ApplicationService applicationService;

@Test
public void testMyMethod() {
Application app = applicationService.getDetailsbyName(properties.getAppName());

assertNotNull(app);
}
}

c. Use @ActiveProfiles to activate a specific profile for testing when you have multiple profiles.

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 com.employee.service.EmployeeService;
import static org.junit.Assert.*;

@SpringBootTest
@ActiveProfiles({"test","dev"})
public class EmployeeServiceTest {

@Autowired
private EmployeeService employeeService;

@Test
public void testGetEmployeeCount() {
int empCount = employeeService.getEmployeeCount();

assertTrue(empCount>0);
}
}

d. Use @DynamicPropertySource to set dynamic configuration properties for tests.

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 com.user.service.UserService;
import com.user.dto.User;
import static org.junit.Assert.*;

@SpringBootTest
public class DynamicPropertyTest {

@Autowired
private UserService userService;

@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("custom.name", () -> "Jack");
}

@Value("${custom.name}")
private String customName;

@Test
public void testGetUserByName() {

User user = userService.getUserByName(customName);

assertNotNull(user);
}
}

e. Use @DirtiesContext to reset the Spring context after a test.

If you have tests that modify the Spring context, such as adding or modifying beans, you may need to reset the context after each test to ensure that subsequent tests start with a clean context. @DirtiesContext can be useful in scenarios where you need to ensure that each test method runs with a fresh and isolated application context.

  1. Usage:
  • You can annotate individual test methods or entire test classes with @DirtiesContext.
  • When applied to a test method, only that specific test method will trigger the context reset.
  • When applied to a test class, it affects all test methods within that class, causing the context to be reset after each test method.

2. Use Cases:

  • @DirtiesContext is typically used when test methods have side effects on the Spring application context that can't be undone by regular transactional or rollback mechanisms.
  • For example, if a test method modifies a singleton bean's state in a way that affects subsequent tests, use @DirtiesContext to reset the context.

3. Impact:

  • Resetting the application context can be relatively expensive in terms of performance, so use it judiciously.
  • It should not be used as the default approach for all tests. Instead, consider using it only when necessary to address specific testing challenges.

f. Use RestTemplate to make HTTP requests.

RestTemplate is used for making HTTP requests to interact with RESTful web services or APIs.

  • Typically used in production code.
  • Supports making actual HTTP requests to external services.
  • Typically used for interacting with real services in a running application.
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.HttpStatusCode;
import static org.junit.Assert.*;

@SpringBootTest
public class ExternalServiceTest {

@Test
public void testExampleApi() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://api.example.com/data";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

assertFalse(response.getStatusCode() == HttpStatusCode.valueOf(404))
}
}

g. Use @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) to start the server on a random port. Use TestRestTemplate to make HTTP requests and assert responses.

If you need to test your application’s integration with an external system, such as a database or another microservice, you might have to start the server on a random port.

TestRestTemplate is a subclass of RestTemplate, specifically designed for integration testing of Springboot applications. It allows you to make HTTP requests to your application’s RESTful endpoints as if you were an external client.

  • Typically used in integration tests.
  • Allows you to test your application’s endpoints without making actual external requests.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.client.TestRestTemplate.HttpClientOption;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class LocalEndpointTest {

@LocalServerPort
private int port;

@Autowired
private TestRestTemplate restTemplate;

@Test
public void testGetBusinesses(){
String url = "http://localhost:" + port + "/getBusinesses";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("Expected response body");
}
}

h. Use @Sql to execute SQL scripts before and after a test.

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
import org.junit.jupiter.api.Test;

@SpringBootTest
public class IntegrationTest {

@Test
@Sql(scripts = { "/init-database.sql", "/populate-data.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public void testWithSqlScripts() {
// Sql scripts get executed before the execution of this block
}

@Test
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testWithCleanupSql() {
// Sql scripts get executed after the execution of this block
}
}

i. Use @Disabled to disable a test temporarily.

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import com.employee.service.EmployeeService;
import com.employee.dto.Employee;
import static org.junit.Assert.*;

@SpringBootTest
public class EmployeeServiceTest {

@Autowired
private EmployeeService employeeService;

@Disabled("Temporarily disabled until bug XYZ is fixed")
@Test
public void testGetEmployees() {
// Test will be skipped
Employee employee = employeeService.getEmployeeByName("Harry");
assertNotNull(employee);
}

@Test
public void testGetEmployeeCount() {
// Test will be executed
int empCount = employeeService.getEmployeeCount();
assertTrue(empCount>0);
}
}

The entire class can also be annotated as @Disabled.

j. Use @RepeatedTest to repeat a test a specified number of times.

import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import com.user.service.UserService;
import static org.junit.Assert.*;

@SpringBootTest
public class UserServiceTest {

@Autowired
private UserService userService;

@RepeatedTest(5) //This test will run 5 times
public void testGetUserCount() {
int count = userService.getUserCount();
assertTrue(count>0);
}

@RepeatedTest(3) //This test will run 3 times
public void testGetLoggedInUsers(RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();

// Test logic, using currentRepetition and totalRepetitions
RestTemplate restTemplate = new RestTemplate();
String url = "http://api.example.com/getLoggedInUsers";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if(response.getBody() != null) {
System.out.println("CurrentRepetition: " + currentRepetition);
}
}
}

4. Test Coverage

Use code coverage tools like SonarQube or Jacoco to measure the coverage of your tests. Aim for high code coverage to ensure most of your code is tested.

Testing Frameworks

There are several testing tools commonly used in Spring Boot applications, including:

  1. JUnit: A widely-used testing framework for Java.
  2. Mockito: A mocking framework for creating mock objects in tests.
  3. Testcontainers: Provides lightweight, throwaway instances of common databases or services for testing.
  4. Spring Boot Test: Offers annotations and utilities for testing Spring Boot applications.
  5. RestAssured: A Java DSL for simplifying the testing of REST services.
  6. Selenium: Used for web application testing, especially end-to-end testing.
  7. Jacoco: A code coverage analysis tool that helps you identify areas of your codebase that need more testing.
  8. WireMock: A tool for mocking HTTP services, useful for testing external service interactions.

Conclusion

By following best practices and leveraging the right testing tools, you can ensure that your tests are reliable, maintainable, and effective in finding bugs in your application. Consistent testing will lead to improved code quality, fewer bugs, and happier users.

Happy Learning!

Stay tuned with the Simform Engineering blog for more updates on the latest tools and technologies.

Follow us: Twitter | LinkedIn

--

--