Test-Driven Development in Spring Boot using Pyramid Approach
Introduction
In this post, we will follow the test pyramid approach in order to develop a simple Spring Boot application, which has the following classes: Controller, Service, Repository, and Domain. We will focus on Unit and Integration Tests for the Controller, Service, Repository classes.
Testing Service Components
We start with the service component is the application logic is usually written in the service component.
Unit Test
We use @RunWith and @SpringBootTest annotations for the unit test class. We don’t load the controller but load the service and its dependencies. WebEnvironment.NONE mode enables that the application should not run as a web application and should not start an embedded web server. MockitoJUnitRunner.class initializes the Mock objects, which is UserRepository.
@RunWith(MockitoJUnitRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class UserServiceUnitTest { ... }
@Mock annotation is used to inject the repository. We mock the data as we don’t care about the actual data.
@Mock
private UserRepository userRepository;
@InjectMocks annotation is used to inject the service
@InjectMocks
private UserService userService;
@Before annotation causes that method to be run before each @Test method. If we didn’t use @RunWith(MockitoJUnitRunner.class) in the class definition, we had to call MockitoAnnotations.initMocks(this) method to initialize annotated fields.
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class UserServiceUnitTest {
...
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
...
}
Here is how we test the service and verify the result.
@Test public void testRetrieveAllUsersHappyPath() {
// Create a user
User aMockUser = new User();
... set fields here when(userRepository.save(any(User.class))).thenReturn(aMockUser);
// Save the user
User newUser = userService.createUser(aMockUser); // Verify the save
assertEquals("DummyName", newUser.getName());
}
Integration Test
The integration test involves the interaction between the UserService and UserRepository. We don’t want to run any of the controllers as we only want to test the service component. We just want to access the service and data access components. We use the same configurations forUserServiceIntegrationTest.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class UserServiceIntegrationTest {
...
}
@Autowired annotation injects the service that we want to test
@Autowired
private UserService userService;
Now we can test the UserService straight away
@Test
public void testCreateUserHappyPath() { // Create User
User aMockUser = new User();
... set fields here // Test adding the user
User newUser = userService.createUser(aMockUser); // Verify the addition
assertNotNull(newUser);
}
Testing Controllers
Unit Test
@WebMvcTest only scans the controller that we want to test
@RunWith(SpringRunner.class)
@WebMvcTest(value = UserController.class)
public class UserControllerUnitTest {
...
}
We inject the MockMvc to perform a request and the UserService in order to stub the user service
@Autowired
private MockMvc mockMvc; @MockBean
private UserService userService;
We create a mock user and simulate the form submit (POST) using the MockMvc object.
@Test
public void testCreateUserHappyPath() throws Exception {
User aMockUser = new User();
...set fields here
when(userService.createUser(any(User.class)))
.thenReturn(aMockUser);
// simulate the form submit (POST)
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(aMockUser))
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andReturn();
}
Integration Test
Since we want to simulate the real environment, we use WebEnvironment.RANDOM_PORT to create a web application context (which usually triggers listening on a random port).
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTest {
...
}
We inject the controller
@Autowired
UserController userController;
We create a mock user, post the User to the controller and then assert that the outcome is as expected
@Test
public void testCreateUserHappyPath() {
User aMockUser = new User();
..set fields here
// POST the User bean to the controller; check the outcome
ResponseEntity<User> newUser = userController.createUser(aMockUser);
// Assert that the outcome is as expected
assertThat(newUser.getStatusCode(), is(equalTo(HttpStatus.CREATED)));
}
Testing Repository
Integration Test
An integration test is enough for the repository test. First, we add the following annotations to the class.
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class UserRepositoryIntegrationTest { ... }
We inject TestEntityManager and UserRepository
@Autowired
private TestEntityManager entityManager; @Autowired
private UserRepository userRepository;
We set up data scenario, save test data with TestEntityManager object, find an inserted record using UserRepository object, and then assert that the outcome is as expected
@Test
public void testFindBynameHappyPath() {
// setup data scenario
User aMockUser = new User();
.. set fields here
// save test data
entityManager.persist(aMockUser);
// Find an inserted record
User foundUser = userRepository.findByname("DummyName");
assertThat(foundUser.getBirthDate().toString(),
is(equalTo("2000-04-01")));
}
Create Test Suites
We can also create a test suite to run all the desired tests per future. Here is an example of all integration tests for “Create User Feature Test Suit”. Test Suite setup (annotations) is sufficient
@RunWith(Suite.class)
@Suite.SuiteClasses({UserServiceIntegrationTest.class,UserRepositoryIntegrationTest.class,UserControllerIntegrationTest.class })
public class CreateUserFeatureTestSuite {
// intentionally empty - Test Suite setup (annotations) is sufficient
}
Summary
In this post, we learned how to create Unit and Integration Tests for the Controller, Service, Repository classes for a simple Spring Boot application. You can find the source code on GitHub.
Originally published at https://github.com.