This blog post explains the process to Unit test and Integration test Spring Boot application with JUnit and Spring Testing library
Unit Tests
Typical Spring Boot application divided into 3 layers
- Controller or Web
- Service
- Repository or DAO
Repository layer Testing
Let’s start with Repository layer. See below example.
@ExtendWith(SpringExtension.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE
@ContextConfiguration(classes = {MyApplication.class})
@ImportAutoConfiguration(RefreshAutoConfiguration.class)
@ActiveProfiles(value = "local")
class EmployeeRepositoryTest
{
@Autowired
private EmployeeRepository employeeRepository;
@Test
void getAllEmployees()
{
List<Employee> employeesList=employeeRepository.findAll();
employeesList.forEach(employee -> System.out.println(employee.getName()));
assertNotNull(employeesList);
}
}
The above test class show cases Employee Repository class. We use DataJpaTest
annotation for this. Here are the step by step instructions
- Always use
DataJpaTest
for Repository later tests - Disable Auto Configuring Test Database if you want to use existing database
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
. Otherwise just use@AutoConfigureTestDatabase
- Define Main Class, Properties and Config classes in
@ContextConfiguration
4. If you define RefreshScope scope in your code, use @ImportAutoConfiguration(RefreshAutoConfiguration.class)
to auto import the config for RefreshScope
5. Define or select profile using @ActiveProfiles(value = "local")
If use password vault like Hashicorp vault, make sure to pass the role_id
and secret_id
during test start up. See below example for IntelliJ IDE tests
Service Layer Testing
If we want to test service layer, we need to mock Repository layer and build Service class. See below example for EmployeeServiceTest
class
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles(value = "local")
@AutoConfigureMockMvc
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class EmployeeServiceTest
{
@Mock
EmployeeRepository employeeRepository;
@Mock
ProtocolRepository protocolRepository;
@Mock
EmployeeTypeRepository employeeTypeRepository;
EmployeeService employeeService;
@BeforeEach
void initTestCase()
{
employeeService = new EmployeeServiceImpl(employeeRepository, protocolRepository, employeeTypeRepository);
}
@Test
void getAllEmployees()
{
when(employeeRepository.findAll()).thenReturn(Collections.singletonList(new Employee(1L)));
System.out.println(employeeService.findEmployeeById(1L).getName());
assertNotNull(employeeService.findEmployeeById(1L));
}
}
The above test class show cases Employee Service class. Here are the step by step instructions
- Use
SpringBootTest
for Service layer tests and make sure to use(webEnvironment = RANDOM_PORT)
to let system select random port during start up - Define or select profile using
@ActiveProfiles(value = "local")
- Auto configure MockMvc using
AutoConfigureMockMvc
annotation. - Define
TestInstance
Class, to configure the lifecycle of test instances for the annotated test class or test interface. IfTestInstance
is not explicitly declared on a test class or on a test interface implemented by a test class, the lifecycle mode will implicitly default to PER_METHOD.
4. If you define RefreshScope scope in your code, use @ImportAutoConfiguration(RefreshAutoConfiguration.class)
to auto import the config for RefreshScope
And at last If use password vault like Hashicorp vault, make sure to pass the role_id
and secret_id
during test start up. See the example in Repository layer Test
Controller or Web layer test
The controller or web layer can be tested using MockMvc.
See below example
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ContextConfiguration(classes = {PresApplication.class})
@WithUserDetails(value = "demo_user", userDetailsServiceBeanName = "customDbUserDetailsService")
@AutoConfigureMockMvc
@ActiveProfiles(value = "local")
class PersonControllerTest
{
@Autowired
private MockMvc mockMvc;
@MockBean
private PersonService personService;
@Test
void getPersonById() throws Exception
{
var personId = 1L;
when(personService.findPersonById(personId)).thenReturn(new Person());
this.mockMvc.perform(get("/api/v1/person/find/" + personId)).andDo(print()).andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
}
The controller tested using MockMvc
which performs REST API request just like Frontend/Mobile application client. The above request looks similar to Service Layer test with 2 changes
- We are mocking PersonService instead of PersonRepository
- Injected demo user into Spring Security using
WithUserDetails
Since theREST API is protected by Spring Security, we use WithUserDetails
annotation to mock user demo_user into Spring Security context. Remember this user must exist in Database.
Integration tests
The integration tests look similar to Controller layer tests but with one difference. Instead of mocking the service layer, the test hits actual Service and Repository layer.
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles(value = "local")
@AutoConfigureMockMvc
@WithUserDetails(value = "jaddap2", userDetailsServiceBeanName = "customDbUserDetailsService")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PersonIntegrationTest
{
@Autowired
private WebApplicationContext context;
@Autowired
private MockMvc mockMvc;
@BeforeAll
public void setup()
{
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
@WithUserDetails(value = "jaddap2", userDetailsServiceBeanName = "customDbUserDetailsService")
void findPatientById() throws Exception
{
MvcResult mvcResult = this.mockMvc.perform(get("/api/v1/person/find/1")).andDo(print()).andExpect(status().isOk()).andReturn();
assertNotNull(mvcResult);
}
}
We can also define WithUserDetails
annotation at method level such that, different users with different access levels can be tested.
Happy Coding 🙂