A Beginners Guide to Unit Testing CRUD Endpoints of a Spring Boot Java Web Service/API

Gabriel Pulga
8 min readOct 19, 2020

--

Overview

In software development, testing each part of a program is crucial to assert that all individual parts are correct.

In the previous article we covered some testing strategies, which you can check it here.

A unit is the smallest testable part of the software and in object-oriented programming it’s also called a method, which may belong to a super class, abstract class or a child class. Either way, unit tests are an important step in the development phase of an application.

Here are some key reasons to not skip the proccess of creating unit tests :

  • They help to fix bugs early in the development cycle and save costs;
  • Understanding the code base is essential and unit tests are some of the best way of enabling developers to learn all they can about the application and make changes quickly;
  • Good unit tests may also serve as documentation for your software;
  • Code is more reliable and reusable because in order to make unit testing possible, modular programming is the standard technique used;

Guidelines

  • Our test case should be independent;
  • Test only one code at a time;
  • Follow clear and consistent naming conventions;
  • Since we are doing unit tests, we need to isolate dependencies. We will use mocks for that. Mocks are objects that “fake”, or simulate, the real object behavior. This way we can control the behavior of our dependencies and test just the code we want to test.

Setting up our environment

In this project, we’ll be working with a CRUD RESTful API that we’ve developed using Spring Boot, if you want to know how we did that, you can click here.

For each operational endpoint, we’ll need to test its controller and service by unitary approach, simulating its expected result and comparing with the actual result through a mock standpoint.

Our testing framework of choice will be JUnit, it provides assertions to identify test method, so be sure to include in your maven pom.xml file :

<! — Junit 5 →<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><scope>test</scope></dependency><dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-launcher</artifactId><scope>test</scope></dependency><! — Mockito extention →<dependency><groupId>org.mockito</groupId><artifactId>mockito-junit-jupiter</artifactId><scope>test</scope></dependency>

Our tests will be grouped in separate folders, the following way :

Unit tests folder separation regarding controllers and services

Testing the Service layer

Our Service layer implements our logic and depends on our Repository so we’ll need to mock its behavior through Mockito annotations and then verify the code with known inputs and outputs.

For a quick recap of our Service layer code that will be tested, these are all our endpoints service classes pasted into one section of code :

@Servicepublic class CreateUserService {@AutowiredUserRepository repository;public User createNewUser(User user) {return repository.save(user);}}@Servicepublic class DeleteUserService {@AutowiredUserRepository repository;public void deleteUser(Long id) {repository.findById(id).orElseThrow(() -> new UserNotFoundException(id));repository.deleteById(id);}}@Servicepublic class DetailUserService {@AutowiredUserRepository repository;public User listUser(Long id) {return repository.findById(id).orElseThrow(() -> new UserNotFoundException(id));}}@Servicepublic class ListUserService {@AutowiredUserRepository repository;public List<User> listAllUsers() {return repository.findAll();}}@Servicepublic class UpdateUserService {@AutowiredUserRepository repository;public User updateUser(Long id, User user) {repository.findById(id).orElseThrow(() -> new UserNotFoundException(id));user.setId(id);return repository.save(user);}}Create a new user serviceStarting with our CreateUserService class, we’ll create a test class named CreateUserServiceTest.src/test/java/com/usersapi/endpoints/unit/service/CreateUserServiceTest.java@RunWith(MockitoJUnitRunner.class)public class CreateUserServiceTest {@Mockprivate UserRepository userRepository;@InjectMocksprivate CreateUserService createUserService;@Testpublic void whenSaveUser_shouldReturnUser() {User user = new User();user.setName(“Test Name”);when(userRepository.save(ArgumentMatchers.any(User.class))).thenReturn(user);User created = createUserService.createNewUser(user);assertThat(created.getName()).isSameAs(user.getName());verify(userRepository).save(user);}}

If you notice in the code, we’re using the assertThat and verify methods to test different things.

By calling the verify method, we’re checking that our repository was called and by calling assertThat we’re checking that our service answered our call with the correct expected value.

Some notes about the annotations used :

  • @RunWith(MockitoJUnitRunner.class) : Invokes the class MockitoJUnitRunner to run the tests instead of running in the standard built in class.
  • @Mock : Used to simulate the behavior of a real object, in this case, our repository
  • @InjectMocks : Creates an instance of the class and injects the mock created with the @Mock annotation into this instance
  • @Test : Tells JUnit that the method to which this annotation is attached can be run as a test case

Our other endpoints will follow the same pattern, with the exception of those that depend upon an id.

List all users service

src/test/java/com/usersapi/endpoints/unit/service/ListUserServiceTest.java

@RunWith(MockitoJUnitRunner.class)public class ListUserServiceTest {@Mockprivate UserRepository userRepository;@InjectMocksprivate ListUserService listUserService;@Testpublic void shouldReturnAllUsers() {List<User> users = new ArrayList();users.add(new User());given(userRepository.findAll()).willReturn(users);List<User> expected = listUserService.listAllUsers();assertEquals(expected, users);verify(userRepository).findAll();}}

Delete an existing user service

For all endpoints that rely upon a given id like this one, we need to throw an exception for when the id doesn’t exist. This exception needs to be also handled on unit tests.

src/test/java/com/usersapi/endpoints/unit/service/DeleteUserServiceTest.java

@RunWith(MockitoJUnitRunner.class)public class DeleteUserServiceTest {@Mockprivate UserRepository userRepository;@InjectMocksprivate DeleteUserService deleteUserService;@Testpublic void whenGivenId_shouldDeleteUser_ifFound(){User user = new User();user.setName(“Test Name”);user.setId(1L);when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));deleteUserService.deleteUser(user.getId());verify(userRepository).deleteById(user.getId());}@Test(expected = RuntimeException.class)public void should_throw_exception_when_user_doesnt_exist() {User user = new User();user.setId(89L);user.setName(“Test Name”);given(userRepository.findById(anyLong())).willReturn(Optional.ofNullable(null));deleteUserService.deleteUser(user.getId());}}

Update an existing user service

src/test/java/com/usersapi/endpoints/unit/service/UpdateUserServiceTest.java

@RunWith(MockitoJUnitRunner.class)public class UpdateUserServiceTest {@Mockprivate UserRepository userRepository;@InjectMocksprivate UpdateUserService updateUserService;@Testpublic void whenGivenId_shouldUpdateUser_ifFound() {User user = new User();user.setId(89L);user.setName(“Test Name”);User newUser = new User();user.setName(“New Test Name”);given(userRepository.findById(user.getId())).willReturn(Optional.of(user));updateUserService.updateUser(user.getId(), newUser);verify(userRepository).save(newUser);verify(userRepository).findById(user.getId());}@Test(expected = RuntimeException.class)public void should_throw_exception_when_user_doesnt_exist() {User user = new User();user.setId(89L);user.setName(“Test Name”);User newUser = new User();newUser.setId(90L);user.setName(“New Test Name”);given(userRepository.findById(anyLong())).willReturn(Optional.ofNullable(null));updateUserService.updateUser(user.getId(), newUser);}}

List an existing user service

src/test/java/com/usersapi/endpoints/unit/service/DetailUserServiceTest.java

@RunWith(MockitoJUnitRunner.class)public class DetailUserServiceTest {@Mockprivate UserRepository userRepository;@InjectMocksprivate DetailUserService detailUserService;@Testpublic void whenGivenId_shouldReturnUser_ifFound() {User user = new User();user.setId(89L);when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));User expected = detailUserService.listUser(user.getId());assertThat(expected).isSameAs(user);verify(userRepository).findById(user.getId());}@Test(expected = UserNotFoundException.class)public void should_throw_exception_when_user_doesnt_exist() {User user = new User();user.setId(89L);user.setName(“Test Name”);given(userRepository.findById(anyLong())).willReturn(Optional.ofNullable(null));detailUserService.listUser(user.getId());}}

Testing the Controller layer

Our unit tests regarding our controllers should consist of a request and a verifiable response that we’ll need to check if its what we expected or not.

By using the MockMvcRequestBuilders class, we can build a request and then pass it as a parameter to the method which executes the actual request. We then use the MockMvc class as the main entry point of our tests, executing requests by calling its perform method.

Lastly, we can write assertions for the received response by using the static methods of the MockMvcResultMatchers class.

For a quick recap of our Controller layer code that will be tested, these are all our endpoints controller classes pasted into one section of code :

@RestController@RequestMapping(“/users”)public class CreateUserController {@AutowiredCreateUserService service;@PostMapping@ResponseStatus(HttpStatus.CREATED)public ResponseEntity<User> createNewUser_whenPostUser(@RequestBody User user) {User createdUser = service.createNewUser(user);URI uri = ServletUriComponentsBuilder.fromCurrentRequest().path(“/{id}”).buildAndExpand(createdUser.getId()).toUri();return ResponseEntity.created(uri).body(createdUser);}}@RestController@RequestMapping(“/users/{id}”)public class DeleteUserController {@AutowiredDeleteUserService service;@DeleteMapping@ResponseStatus(HttpStatus.NO_CONTENT)public void deleteUser_whenDeleteUser(@PathVariable Long id) {service.deleteUser(id);}}@RestController@RequestMapping(“/users/{id}”)public class DetailUserController {@AutowiredDetailUserService service;@GetMapping@ResponseStatus(HttpStatus.OK)public ResponseEntity<User> list(@PathVariable Long id) {return ResponseEntity.ok().body(service.listUser(id));}}@RestController@RequestMapping(“/users”)public class ListUserController {@AutowiredListUserService service;@GetMapping@ResponseStatus(HttpStatus.OK)public ResponseEntity<List<User>> listAllUsers_whenGetUsers() {return ResponseEntity.ok().body(service.listAllUsers());}}@RestController@RequestMapping(“/users/{id}”)public class UpdateUserController {@AutowiredUpdateUserService service;@PutMapping@ResponseStatus(HttpStatus.OK)public ResponseEntity<User> updateUser_whenPutUser(@RequestBody User user, @PathVariable Long id) {return ResponseEntity.ok().body(service.updateUser(id, user));}}

Create a new user controller

Applying these steps in our CreateUserControllerTest we should have :

src/test/java/com/usersapi/endpoints/unit/controller/CreateUserControllerTest.java

@RunWith(SpringRunner.class)@WebMvcTest(CreateUserController.class)public class CreateUserControllerTest {@Autowiredprivate MockMvc mockMvc;@MockBeanprivate CreateUserService service;@Testpublic void createUser_whenPostMethod() throws Exception {User user = new User();user.setName(“Test Name”);given(service.createNewUser(user)).willReturn(user);mockMvc.perform(post(“/users”).contentType(MediaType.APPLICATION_JSON).content(JsonUtil.toJson(user))).andExpect(status().isCreated()).andExpect(jsonPath(“$.name”, is(user.getName())));}}

About the @MockBean annotation, it’s from Spring Boot and is being used in our service, as well as being registered in our application context to verify the behavior of the mocked class.

Our JsonUtil class is being used to map and generate a JSON request for our “Create a new user” endpoint :

src/test/java/com/usersapi/endpoints/util/JsonUtil.java

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);}}

List all users controller

src/test/java/com/usersapi/endpoints/unit/controller/ListUserControllerTest.java

@RunWith(SpringRunner.class)@WebMvcTest(ListUserController.class)public class ListUserControllerTest {@Autowiredprivate MockMvc mvc;@MockBeanprivate ListUserService listUserService;@Testpublic void listAllUsers_whenGetMethod()throws Exception {User user = new User();user.setName(“Test name”);List<User> allUsers = Arrays.asList(user);given(listUserService.listAllUsers()).willReturn(allUsers);mvc.perform(get(“/users”).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andExpect(jsonPath(“$”, hasSize(1))).andExpect(jsonPath(“$[0].name”, is(user.getName())));}}

Delete an existing user controller

As done with our service layer unit tests, we’ll need to treat all endpoints that depend upon an id with additional configuration for a possible RunTimeException (UserNotFound) error that we specified previously :

src/test/java/com/usersapi/endpoints/unit/controller/DeleteUserControllerTest.java

@RunWith(SpringRunner.class)@WebMvcTest(DeleteUserController.class)public class DeleteUserControllerTest {@Autowiredprivate MockMvc mvc;@MockBeanprivate DeleteUserService deleteUserService;@Testpublic void removeUserById_whenDeleteMethod() throws Exception {User user = new User();user.setName(“Test Name”);user.setId(89L);doNothing().when(deleteUserService).deleteUser(user.getId());mvc.perform(delete(“/users/” + user.getId().toString()).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isNoContent());}@Testpublic void should_throw_exception_when_user_doesnt_exist() throws Exception {User user = new User();user.setId(89L);user.setName(“Test Name”);Mockito.doThrow(new UserNotFoundException(user.getId())).when(deleteUserService).deleteUser(user.getId());mvc.perform(delete(“/users/” + user.getId().toString()).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isNotFound());}}

List an existing user controller

src/test/java/com/usersapi/endpoints/unit/controller/DetailUserControllerTest.java

@RunWith(SpringRunner.class)@WebMvcTest(DetailUserController.class)public class DetailUserControllerTest {@Autowiredprivate MockMvc mvc;@MockBeanprivate DetailUserService detailUserService;@Testpublic void listUserById_whenGetMethod() throws Exception {User user = new User();user.setName(“Test Name”);user.setId(89L);given(detailUserService.listUser(user.getId())).willReturn(user);mvc.perform(get(“/users/” + user.getId().toString()).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andExpect(jsonPath(“name”, is(user.getName())));}@Testpublic void should_throw_exception_when_user_doesnt_exist() throws Exception {User user = new User();user.setId(89L);user.setName(“Test Name”);Mockito.doThrow(new UserNotFoundException(user.getId())).when(detailUserService).listUser(user.getId());mvc.perform(get(“/users/” + user.getId().toString()).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isNotFound());}}

Update an existing user service

This endpoint differs from others because it needs a message body to be sent along with the request. For this we’ll be using the ObjectMapper class to write into the content.

src/test/java/com/usersapi/endpoints/unit/controller/UpdateUserControllerTest.java

@RunWith(SpringRunner.class)@WebMvcTest(UpdateUserController.class)public class UpdateUserControllerTest {@Autowiredprivate MockMvc mvc;@MockBeanprivate UpdateUserService updateUserService;@Testpublic void updateUser_whenPutUser() throws Exception {User user = new User();user.setName(“Test Name”);user.setId(89L);given(updateUserService.updateUser(user.getId(), user)).willReturn(user);ObjectMapper mapper = new ObjectMapper();mvc.perform(put(“/users/” + user.getId().toString()).content(mapper.writeValueAsString(user)).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andExpect(jsonPath(“name”, is(user.getName())));}@Testpublic void should_throw_exception_when_user_doesnt_exist() throws Exception {User user = new User();user.setId(89L);user.setName(“Test Name”);Mockito.doThrow(new UserNotFoundException(user.getId())).when(updateUserService).updateUser(user.getId(), user);ObjectMapper mapper = new ObjectMapper();mvc.perform(put(“/users/” + user.getId().toString()).content(mapper.writeValueAsString(user)).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isNotFound());}}

Endnotes

Through testing we learn about our application and make sure its code is safe and reliable.

Next, let’s add some integration tests, since we also want to test the integration, and not just mock their behavior.

We hope this article was useful for you, if you’d like to ask any question about it, make sure to contact us.

Source code

You can find all the source code from this article available in our github page here.

Authorship

This article was originally written by Gabriel Pulga and Gibran Pulga on codefiction.net and can also be found here.

--

--

Gabriel Pulga

DevOps/SRE Engineer from Brazil. Check out my github at @gabrielpulga