A Beginners Guide to Unit Testing CRUD Endpoints of a Spring Boot Java Web Service/API
By: Gabriel Pulga
Tags: Code, Backend Development, Software Testing, Spring, Restful Api

A Beginners Guide to Unit Testing CRUD Endpoints of a Spring Boot Java Web Service/API Gabriel Pulga Oct 19, 2020·8 min read

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;

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;

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;

code at a time; Follow clear and consistent naming conventions;

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 :

@Service public class CreateUserService { @Autowired UserRepository repository; public User createNewUser(User user) { return repository.save(user); } } @Service public class DeleteUserService { @Autowired UserRepository repository; public void deleteUser(Long id) { repository.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); repository.deleteById(id); } } @Service public class DetailUserService { @Autowired UserRepository repository; public User listUser(Long id) { return repository.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); } } @Service public class ListUserService { @Autowired UserRepository repository; public List<User> listAllUsers() { return repository.findAll(); } } @Service public class UpdateUserService { @Autowired UserRepository 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 service Starting 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 { @Mock private UserRepository userRepository; @InjectMocks private CreateUserService createUserService; @Test public 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 { @Mock private UserRepository userRepository; @InjectMocks private ListUserService listUserService; @Test public 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 { @Mock private UserRepository userRepository; @InjectMocks private DeleteUserService deleteUserService; @Test public 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 { @Mock private UserRepository userRepository; @InjectMocks private UpdateUserService updateUserService; @Test public 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 { @Mock private UserRepository userRepository; @InjectMocks private DetailUserService detailUserService; @Test public 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 { @Autowired CreateUserService 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 { @Autowired DeleteUserService service; @DeleteMapping @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUser_whenDeleteUser(@PathVariable Long id) { service.deleteUser(id); } } @RestController @RequestMapping(“/users/{id}”) public class DetailUserController { @Autowired DetailUserService 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 { @Autowired ListUserService service; @GetMapping @ResponseStatus(HttpStatus.OK) public ResponseEntity<List<User>> listAllUsers_whenGetUsers() { return ResponseEntity.ok().body(service.listAllUsers()); } } @RestController @RequestMapping(“/users/{id}”) public class UpdateUserController { @Autowired UpdateUserService 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 { @Autowired private MockMvc mockMvc; @MockBean private CreateUserService service; @Test public 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 { @Autowired private MockMvc mvc; @MockBean private ListUserService listUserService; @Test public 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 { @Autowired private MockMvc mvc; @MockBean private DeleteUserService deleteUserService; @Test public 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()); } @Test public 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 { @Autowired private MockMvc mvc; @MockBean private DetailUserService detailUserService; @Test public 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()))); } @Test public 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 { @Autowired private MockMvc mvc; @MockBean private UpdateUserService updateUserService; @Test public 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()))); } @Test public 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.