Creating a strong foundation for a new project using Spring Boot – Part 2

Contents

This post is divided into four parts.

  1. Getting Ready
  2. Unit testing
  3. Integration testing
  4. Launching the app

Unit tests

The “spring-boot-starter-test” is automatically added when you create a new project. If you are familiar with Spring test, you might be aware of @SpringBootTest annotation. But that’s an overkill for a unit test. @SpringBootTest has the power to start an embedded container, which can be useful in integration tests, but more than required while testing the DAO layer. Since we are testing small “units” of the application of the application, we need to be focused on testing more specific “slices” (Controllers, Service layer, DAO layer). Luckily, it’s really easy to do this.

Let’s create our first test.

Testing the DAO layer

I know we have not written any DAO layer code in our application, still we can verify if the repositories are configured properly. Moreover, as the application grows, you might feel the need to have unit tests for the DAO layer. Starting early will help in this case.

UserRepositoryTest.java

[code language=”java”]
package org.websandbox.learning.layout.user;

//imports omitted

@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryTest {

@Autowired
TestEntityManager entityManager;

@Autowired
UserRepository userRepository;

@Test
public void searchExistingUserByEmail() {
User user = new User("test");
entityManager.persistAndFlush(user);

Optional<User> fromDb = userRepository.findByName(user.getName());
assertThat(fromDb.get().getName()).isEqualTo(user.getName());
}

@Test(expected = NoSuchElementException.class)
public void searchNonExistingUserByEmail() {
Optional<User> fromDb = userRepository.findByName("doesNotExist");
fromDb.get();
}

@Test
public void searchExistingUserById() {
User user = new User("test");
entityManager.persistAndFlush(user);

Optional<User> fromDb = userRepository.findById(user.getId());
assertThat(fromDb.get().getName()).isEqualTo(user.getName());
}

@Test(expected = NoSuchElementException.class)
public void searchNonExistingUserById() {
Optional<User> fromDb = userRepository.findById(-11L);
fromDb.get();
}

@Test
public void findAllUsers() {
User alex = new User("alex");
User ron = new User("ron");
User bob = new User("bob");

entityManager.persist(alex);
entityManager.persist(bob);
entityManager.persist(ron);
entityManager.flush();

List<User> allUsers = userRepository.findAll();

assertThat(allUsers).hasSize(3).extracting(User::getName).containsOnly(alex.getName(), ron.getName(),
bob.getName());
}

}
[/code]

@RunWith(SpringRunner.class) – tells JUnit to run using Spring’s testing support

@DataJpaTest will do the following:

  • Configure an in-memory database.
  • Auto-configure Hibernate, Spring Data and the DataSource.
  • Perform an @EntityScan.
  • Turn on SQL logging

TestEntityManager – The TestEntityManager in the above test is provided by Spring Boot. It’s an alternative to the standard JPA EntityManager that provides methods commonly used when writing tests.

assertThat(…) – This comes from a fluent assertion library assertj. Learn more about assertj in this post.

Testing the service layer

As we do not need the DAO layer while unit testing the service layer, it’s a good idea to mock the DAO layer.

UserServiceImplTest.java

[code language=”java”]
package org.websandbox.learning.layout.user;

//imports omitted

@RunWith(SpringRunner.class)
public class UserServiceImplTest {

@TestConfiguration
static class UserServiceImplTestContextConfiguration {
@Bean
public UserService userService() {
return new UserServiceImpl();
}

@Bean
public UserRepository userRepository() {
return Mockito.mock(UserRepository.class);
}
}

@Autowired
UserService userService;

@Autowired
UserRepository userRepository;

@Before
public void setUp() {
User john = new User("john");
john.setId(11L);

Optional<User> user = Optional.of(john);

User bob = new User("bob");
User alex = new User("alex");

List<User> allUsers = Arrays.asList(john, bob, alex);

Mockito.when(userRepository.findByName(john.getName())).thenReturn(user);
Mockito.when(userRepository.findByName("wrong_name")).thenReturn(Optional.empty());
Mockito.when(userRepository.findById(john.getId())).thenReturn(user);
Mockito.when(userRepository.findAll()).thenReturn(allUsers);
Mockito.when(userRepository.findById(-99L)).thenReturn(Optional.empty());
}

@Test
public void givenValidNameUserShouldBeFound() {
Optional<User> userFromDb = userService.getUserByName("john");
assertThat(userFromDb.get().getName()).isEqualTo("john");

verifyFindByNameIsCalledOnce("john");
}

@Test(expected = NoSuchElementException.class)
public void givenInValidNameUserShouldNotBeFound() {
Optional<User> userFromDb = userService.getUserByName("wrong_name");
userFromDb.get();

verifyFindByNameIsCalledOnce("wrong_name");
}

@Test
public void givenValidNameUserShouldExist() {
boolean doesUserExist = userService.exists("john");
assertThat(doesUserExist).isEqualTo(true);

verifyFindByNameIsCalledOnce("john");
}

@Test
public void givenNonExistingNameUserShouldNotExist() {
boolean doesUserExist = userService.exists("some_name");
assertThat(doesUserExist).isEqualTo(false);

verifyFindByNameIsCalledOnce("some_name");
}

@Test
public void givenValidIdUserShouldBeFound() {
Optional<User> userFromDb = userService.getUserById(11L);
assertThat(userFromDb.get().getName()).isEqualTo("john");

verifyFindByIdIsCalledOnce();
}

@Test(expected = NoSuchElementException.class)
public void givenInValidIdUserShouldNotBeFound() {
Optional<User> userFromDb = userService.getUserById(-99L);
verifyFindByIdIsCalledOnce();
userFromDb.get();
}

@Test
public void findAllUsers() {
User alex = new User("alex");
User john = new User("john");
User bob = new User("bob");

List<User> allUsers = userService.getAllUsers();
verifyFindAllUsersIsCalledOnce();
assertThat(allUsers).hasSize(3).extracting(User::getName).contains(alex.getName(), john.getName(),
bob.getName());

}

private void verifyFindByNameIsCalledOnce(String name) {
Mockito.verify(userRepository, VerificationModeFactory.times(1)).findByName(name);
Mockito.reset(userRepository);
}

private void verifyFindByIdIsCalledOnce() {
Mockito.verify(userRepository, VerificationModeFactory.times(1)).findById(Mockito.anyLong());
Mockito.reset(userRepository);
}

private void verifyFindAllUsersIsCalledOnce() {
Mockito.verify(userRepository, VerificationModeFactory.times(1)).findAll();
Mockito.reset(userRepository);
}
}
[/code]

@TestConfiguration – If your application uses component scanning, for example, if you use @SpringBootApplication or @ComponentScan, you may find components or configurations created only for specific tests accidentally get picked up everywhere.

To help prevent this, Spring Boot provides @TestComponent and @TestConfiguration annotations that can be used on classes in src/test/java to indicate that they should not be picked up by scanning.

The two beans in the UserServiceImplTestContextConfiguration class are autowired. Notice that the UserRepository class is mocked.

Testing the controllers

The controllers need the containers in order to be tested. However, we do not need the service layer here.

UserRestControllerTest.java

[code language=”java”]
package org.websandbox.learning.layout.user;

//imports omitted

@RunWith(SpringRunner.class)
@WebMvcTest(UserRestController.class)
public class UserRestControllerTest {

@Autowired
MockMvc mvc;

@MockBean
UserService service;

@Before
public void setUp() throws Exception {
}

@Test
public void createUser() throws Exception {
User alex = new User("alex");
given(service.save(Mockito.anyObject())).willReturn(alex);

mvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(JsonUtil.toJson(alex)))
.andExpect(status().isCreated()).andExpect(jsonPath("$.name", is("alex")));
verify(service, VerificationModeFactory.times(1)).save(Mockito.anyObject());
reset(service);
}

@Test
public void getAllUsers() throws Exception {
User alex = new User("alex");
User john = new User("john");
User bob = new User("bob");

List<User> allUsers = Arrays.asList(alex, john, bob);

given(service.getAllUsers()).willReturn(allUsers);

mvc.perform(get("/api/users").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(3))).andExpect(jsonPath("$[0].name", is(alex.getName())))
.andExpect(jsonPath("$[1].name", is(john.getName())))
.andExpect(jsonPath("$[2].name", is(bob.getName())));
verify(service, VerificationModeFactory.times(1)).getAllUsers();
reset(service);
}

}
[/code]

To test Spring MVC controllers are working as expected you can use the @WebMvcTest annotation. @WebMvcTest will auto-configure the Spring MVC infrastructure and limit scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver. Regular @Component beans will not be scanned when using this annotation.

Often @WebMvcTest will be limited to a single controller and used in combination with @MockBean to provide mock implementations for required collaborators.

@WebMvcTest also auto-configures MockMvc. Mock MVC offers a powerful way to quickly test MVC controllers without needing to start a full HTTP server.


JsonUtil.java

[code language=”java”]
package org.websandbox.learning.springdata.util;

//imports omitted

public class JsonUtil {
public static byte[] toJson(Object object) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper.writeValueAsBytes(object);
}
}
[/code]

 

A final note

You need to @Ignore the BasicLayoutApplicationTests before you launch the unit tests. This is because the @SpringBootTest annotation starts the container, loads the application.properties file and tries to connect to the DB which we don’t want for the unit tests.

Launch the unit tests

Navigate to the project directory using the console and execute the following command.

[code language=”plain”]
gradlew test
[/code]

At this point, you all the unit tests should pass and the reports should be generated under the build/reports/tests directory.

Checkpoint

unit-tests

Continue reading Part 3.