It’s no doubt that unit testing is integral part of any software development projects, as it helps detect problems in early stage when small, independent units are being developed. In REST API development, it’s strongly recommended to do unit testing your APIs before integration and system tests.

In this tutorial, you’ll learn how to code unit tests for REST APIs with Spring framework and related technologies (Spring Boot, Spring MVC, Spring Test, JUnit, Mockito,…). I’ll start by explaining the basic concepts and principles, which will be then demonstrated with some real life code examples: unit testing for user management REST APIs (Test add, get, list, update and delete operations).

Table of Content:

  1. What is Unit Testing for REST APIs?
  2. How Unit Testing REST APIs with Spring?
  3. The Sample User APIs
  4. Test Add User API
  5. Test Get User API
  6. Test List Users API
  7. Test Update User API
  8. Test Delete API

 

1. What is Unit Testing for REST APIs?

In REST APIs development with Spring framework, unit testing the APIs is the process of coding and running unit tests for REST controllers to ensure that the APIs process requests and produce responses as expected. Given the following code example of a typical Add user API:

package net.codejava;

// ...

@RestController
@RequestMapping("/users")
public class UserApiController {
	private UserService service;
	private ModelMapper modelMapper;
	
	// ...

	@PostMapping
	public ResponseEntity<?> add(@RequestBody @Valid User user) {
		User persistedUser = service.add(user);
		UserDTO userDto = entity2Dto(persistedUser);
		
		URI uri = URI.create("/users/" + userDto.getId());
		
		return ResponseEntity.created(uri).body(userDto);
	}
	
	// ... 
}

Here, the handler method add() implements the Add User API. So we should write at least two unit tests for the following cases:

- A request contains invalid data (e.g. user email is empty), thus the API should return status 400 (Bad Request).

- A request contains valid data, then the API should return status 200 (OK) with response body is a JSON document that represents user information.

An important note here: the controller layer invokes the service layer to complete the operation, e.g. the method add() of the UserService class is used to persist a User object into database. In REST API unit testing, we test only code of the controller layer - so the service layer is supposed to be always right (which should be unit tested separately, e.g. testing service layer).


2. How Unit Testing REST APIs with Spring?

Spring Boot provides the @WebMvcTest annotation that can be used for a Spring MVC test that focuses only on Spring MVC components such as REST controllers. The following is basic structure of a test class for testing APIs of the above UserApiController class:

@WebMvcTest(UserApiController.class)
public class UserApiControllerTests {
	
	@Autowired private MockMvc mockMvc;
	
	@Autowired private ObjectMapper objectMapper;
	
	@MockBean private UserService service;
	
	@Test
	public void testAddUserAPI() throws Exception {
		
		// create a new User object
		
		// serialize User object to JSON string using ObjectMapper
		
		// use Mockito APIs to mock method calls on UserService object
		
		// use MockMVC to perform request (make API call)
		
		// use MockMvcResultMatchers to assert the response (status code, content type, JSON fields,...)
	}
	
}

You see, this class is annotated using the @WebMvcTest annotation with the controller class is UserApiController - that means when a test method is executed, it will start a Spring Boot application that loads only the specified REST controller (a Spring MVC component) - other components annotated with @Service, @Component, @Repository… won’t be loaded. In other words, it will disable full auto-configuration and instead apply only configuration relevant to MVC tests.

That’s why with this kind of test, we need to use Mockito - a mocking framework for unit tests - in order to create fake objects of the service class used by the controller. I’ll explain further in the following code examples.


3. The Sample User APIs

Suppose that we’re implementing some REST APIs about user management. Let’s review code of the layers: repository, service and controller. Below is code of the User entity class:

package net.codejava;

import jakarta.persistence.*;

import jakarta.validation.constraints.*;

@Entity @Table(name = "users")
public class User {

	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column(length = 50, nullable = false, unique = true)
	@NotBlank(message = "E-mail address must not be empty")
	@Email(message = "User must have valid email address")
	private String email;

	@Column(length = 20, nullable = false)
	private String firstName;

	@Column(length = 20, nullable = false)
	private String lastName;

	@Column(length = 10, nullable = false)
	private String password;

	// getters and setters...

	// equals() and hashCode() based on field id...
}

We use ModelMapper for mapping between entity and DTO, so below is code of the UserDTO class:

package net.codejava;

public class UserDTO {
	private Long id;

	private String email;

	private String firstName;

	private String lastName;

	// getters and setters...

}

Spring Data JPA is used for the persistence layer, so below is code of the UserRepository interface:

package net.codejava;

import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {

}

And code of the service layer is as follows:

package net.codejava;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
	@Autowired private UserRepository repo;

	public User add(User user) {
		return repo.save(user);
	}

	public User update(User user) throws UserNotFoundException {
		if (!repo.existsById(user.getId())) {
			throw new UserNotFoundException();
		}

		return repo.save(user);
	}

	public User get(Long id) throws UserNotFoundException {
		Optional<User> result = repo.findById(id);

		if (result.isPresent()) {
			return result.get();
		}

		throw new UserNotFoundException();
	}

	public List<User> list() {
		return (List<User>) repo.findAll();
	}

	public void delete(Long id) throws UserNotFoundException {
		if (repo.existsById(id)) {
			repo.deleteById(id);
		}

		throw new UserNotFoundException();
	}
}

The type UserNotFoundException is a simple exception class:

package net.codejava;

public class UserNotFoundException extends Exception {

}

And below is code of the controller class that implements REST APIs for user management (I’ll show the detailed code of each handler method later):

package net.codejava;

import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;

import org.modelmapper.ModelMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/users")
public class UserApiController {
	private UserService service;
	private ModelMapper modelMapper;

	protected UserApiController(UserService service, ModelMapper mapper) {
		this.service = service;
		this.modelMapper = mapper;
	}

	@PostMapping
	public ResponseEntity<?> add(...) {
		// ...
	}

	@GetMapping("/{id}")
	public ResponseEntity<?> get(@PathVariable("id") Long id) {
		// ...
	}

	@GetMapping
	public ResponseEntity<?> list() {
		// ...
	}

	@PutMapping("/{id}")
	public ResponseEntity<?> update(...) {
		// ...
	}

	@DeleteMapping("/{id}")
	public ResponseEntity<?> delete(...) {
		// ...
	}

	private UserDTO entity2Dto(User entity) {
		return modelMapper.map(entity, UserDTO.class);
	}

	private List<UserDTO> list2Dto(List<User> listUsers) {
		return listUsers.stream().map(
				entity -> entity2Dto(entity))
					.collect(Collectors.toList());
	}
}

So, as you can see, this REST controller provides the following APIs:

  • POST /users: adds a new user to the system
  • GET /users/{id}: gets details of a specific user by ID
  • GET /users: returns a list of all users in the system
  • PUT /users/{id}: updates details of a user specified by ID
  • DELETE /users/{id}: removes a user from the system

You may also notice that we use Jakarta Bean validation for validating request body.

Next, let’s see how to code unit tests for each of these API operations.


4. Test Add User API

Let’s write a couple of unit tests for the Add User API. The implementation code in the controller is as follows:

@PostMapping
public ResponseEntity<?> add(@RequestBody @Valid User user) {
	User persistedUser = service.add(user);
	UserDTO userDto = entity2Dto(persistedUser);

	URI uri = URI.create("/users/" + userDto.getId());

	return ResponseEntity.created(uri).body(userDto);
}

As you can see, the add operation will return status 201 Created if the user information in the request body is valid and the User object is persisted into the database successfully. If the user information in the request body contain invalid values (as validated by the constraint annotations used in the User entity class), it will return status 400 Bad Request.

That means we need to write 2 unit tests for testing these 2 cases. In the first case, we expect the response status is 400 Bad Request when the request body contains invalid user information (email and first name are empty; other fields not specified). So below is the code:

@WebMvcTest(UserApiController.class)
public class UserApiControllerTests {
	private static final String END_POINT_PATH = "/users";

	@Autowired private MockMvc mockMvc;
	@Autowired private ObjectMapper objectMapper;
	@MockBean private UserService service;

	@Test
	public void testAddShouldReturn400BadRequest() throws Exception {
		User newUser = new User().email("").firstName("");

		String requestBody = objectMapper.writeValueAsString(newUser);

		mockMvc.perform(post(END_POINT_PATH).contentType("application/json")
				.content(requestBody))
				.andExpect(status().isBadRequest())
				.andDo(print())
		;
	}

}

Let me explain the code in this handler:

  • We use the method writeValueAsString() of ObjectMapper class to serialize (convert) a Java object (newUser) to a JSON string (requestBody), which will be sent along with the request.
  • We use the method perform() of MockMVC class to make API call (perform HTTP request).
  • The method post() specifies the HTTP method is POST
  • The method contentType() sets the content type of the request
  • The method content() sets a JSON string as request body - it will be de-serialized to a User object on the server side
  • The method andExpect() performs an expectation (assertion). In this test, we expect the response status is 400 (Bad request)
  • The method andDo() performs a general action. In this case, we print the details of request and response

Note that the post(), content(), status() and print() are static methods in the MockMvcRequestBuilders class under the package org.springframework.test.web.servlet.request.

And in the second case, we expect that the API should return status 201 (Created) because the request body contains valid user information (all fields have valid values). So write the second unit test as follows:

@Test
public void testAddShouldReturn201Created() throws Exception {
	User newUser = new User().email("david.parker@gmail.com")
							 .firstName("David").lastName("Parker")
							 .password("avid808");

	Mockito.when(service.add(newUser)).thenReturn(newUser.id(1L));

	String requestBody = objectMapper.writeValueAsString(newUser);

	mockMvc.perform(post(END_POINT_PATH).contentType("application/json")
			.content(requestBody))
			.andExpect(status().isCreated())
			.andDo(print())
	;

}

An important statement here is that we use Mockito to mock object returned from method call to the service class:

Mockito.when(service.add(newUser)).thenReturn(newUser.id(1L));

This means when the method add() of the UserService class gets invoked, it will return the specified User object (newUser), because as you can see in the controller class, it makes this call:

User persistedUser = service.add(user);

Mockito will create a new mock object of type UserService, and inject into the controller. That’s why we should specify what should be returned when which method is invoked in the test method. Make sense?

Also note that we must specify ID of the newUser object so Mockito will be able to compare it with the User object converted from the JSON string in the request body - in order to decide whether the argument matches or not, in the Mockito.when(…).thenReturn(…) statement. Now you understand why the User entity class must override equals() and hashCode() based on id field.

In addition, you can also verify if the method add() of the UserService class gets called or not, with this statement:

Mockito.verify(service, times(1)).add(newUser);

This is to assert that the add() method must be called exactly one time. And this statement must be called after MockMvc.perform().

Spring Boot REST APIs Ultimate Course

Hands-on REST API Development with Spring Boot: Design, Implement, Document, Secure, Test, Consume RESTful APIs


5. Test Get User API

Below code is the implementation of the Get User API, which returns details of a user found by the given ID:

@GetMapping("/{id}")
public ResponseEntity<?> get(@PathVariable("id") Long id) {
	try {
		User user = service.get(id);
		return ResponseEntity.ok(entity2Dto(user));

	} catch (UserNotFoundException e) {
		e.printStackTrace();
		return ResponseEntity.notFound().build();
	}
}

As you can see, this operation will return status 404 Not Found if no user found with the given ID, or status 200 OK if a user found. So we write the first test for this API, as follows:

@Test
public void testGetShouldReturn404NotFound() throws Exception {
	Long userId = 123L;
	String requestURI = END_POINT_PATH + "/" + userId;

	Mockito.when(service.get(userId)).thenThrow(UserNotFoundException.class);

	mockMvc.perform(get(requestURI))
		.andExpect(status().isNotFound())
		.andDo(print());
}

Here, we use Mockito.when(…).thenThrow(…) to specify that when the method get() of the UserService class gets invoked, it will throw UserNotFoundException, thus we expect the API will return status 404.

Note that the static method get() performs an HTTP GET request.

And for the status 200 OK, we code another test method as follows:

@Test
public void testGetShouldReturn200OK() throws Exception {
	Long userId = 123L;
	String requestURI = END_POINT_PATH + "/" + userId;
	String email = "david.parker@gmail.com";

	User user = new User().email(email)
			 .firstName("David").lastName("Parker")
			 .password("avid808")
			 .id(userId);

	Mockito.when(service.get(userId)).thenReturn(user);

	mockMvc.perform(get(requestURI))
		.andExpect(status().isOk())
		.andExpect(content().contentType("application/json"))
		.andExpect(jsonPath("$.email", is(email)))
		.andDo(print());
}

In this test, we use Mockito.when(…).thenReturn(…) statement to specify that when the method get() of the UserService class gets invoked, it will return a User object, thus we expect the API will return status 200 OK (successful retrieval operation).

And you can see the method jsonPath() is used with XPath-like expression to verity that the JSON document in the response body contain the specified value or not (email field in this case). The response in this test would be like this:

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":123,"email":"david.parker@gmail.com","firstName":"David","lastName":"Parker"}

To learn more about JSON Path expressions, check this page.


6. Test List Users API

The List Users API returns information about users as a list. Below is the implementation code:

@GetMapping
public ResponseEntity<?> list() {
	List<User> listUsers = service.list();
	if (listUsers.isEmpty()) {
		return ResponseEntity.noContent().build();
	}

	return ResponseEntity.ok(list2Dto(listUsers));
}

You see, this operation will return status 204 No Content if no users available in the system, or 200 OK with a list of users in the response body. That means we should write at least 2 unit tests for this API. The first one expects the API will return status 204:

@Test
public void testListShouldReturn204NoContent() throws Exception {
	Mockito.when(service.list()).thenReturn(new ArrayList<>());

	mockMvc.perform(get(END_POINT_PATH))
		.andExpect(status().isNoContent())
		.andDo(print());
}

Here, we use Mockito.when(…).thenReturn(…) statement to specify that when the method list() of the UserService class gets invoked, it will return an empty ArrayList - thus we expect the API will return status 204 indicating no content available.

Next, let’s write code for the second case, as follows:

@Test
public void testListShouldReturn200OK() throws Exception {
	User user1 = new User().email("david.parker@gmail.com")
			 .firstName("David").lastName("Parker")
			 .password("avid808")
			 .id(1L);

	User user2 = new User().email("john.doe@gmail.com")
			 .firstName("John").lastName("Doe")
			 .password("johnoho2")
			 .id(2L);

	List<User> listUser = List.of(user1, user2);

	Mockito.when(service.list()).thenReturn(listUser);

	mockMvc.perform(get(END_POINT_PATH))
		.andExpect(status().isOk())
		.andExpect(content().contentType("application/json"))
		.andExpect(jsonPath("$[0].email", is("david.parker@gmail.com")))
		.andExpect(jsonPath("$[1].email", is("john.doe@gmail.com")))
		.andDo(print());
}

Now, in this test, we return a List collection that contains 2 User objects when the method list() of the UserService class is called, so will expect the API will return status 200 OK. And the response is a JSON document that represents an array of two objects:

[
  {
    "id": 1,
    "email": "david.parker@gmail.com",
    "firstName": "David",
    "lastName": "Parker"
  },
  {
    "id": 2,
    "email": "john.doe@gmail.com",
    "firstName": "John",
    "lastName": "Doe"
  }
]

So, to verify this response, we use the method jsonPath() with an JSON Path expression like this:

.andExpect(jsonPath("$[0].email", is("david.parker@gmail.com")))
.andExpect(jsonPath("$[1].email", is("john.doe@gmail.com")))

The expression $[0].email gets the value of the email field of the first object in the array, and so on. To learn more about JSON Path expressions, check this page.


7. Test Update User API

Next, let’s do unit testing for the Update User API, which is implemented in the REST controller as follows:

@PutMapping("/{id}")
public ResponseEntity<?> update(@PathVariable("id") Long id,
		@RequestBody @Valid User user) {
	try {
		user.setId(id);
		User updatedUser = service.update(user);
		return ResponseEntity.ok(entity2Dto(updatedUser));
	} catch (UserNotFoundException e) {
		e.printStackTrace();
		return ResponseEntity.notFound().build();
	}
}

This operation is used to update details of a user found by the given ID in the URI (the path variable id). It can return 3 different status codes: 404 Not Found, 400 Bad Request and 200 OK. That means we should code at least 3 unit tests.

The first unit test expects the API will return status 404:

@Test
public void testUpdateShouldReturn404NotFound() throws Exception {
	Long userId = 123L;
	String requestURI = END_POINT_PATH + "/" + userId;

	User user = new User().email("david.parker@gmail.com")
						 .firstName("David").lastName("Parker")
						 .password("avid808")
						 .id(userId);

	Mockito.when(service.update(user)).thenThrow(UserNotFoundException.class);

	String requestBody = objectMapper.writeValueAsString(user);

	mockMvc.perform(put(requestURI).contentType("application/json").content(requestBody))
		.andExpect(status().isNotFound())
		.andDo(print());		
}

As you can see, the static method put() is used to perform an HTTP PUT request.

And below is code of the second test case which we expect the API will return status 400 Bad Request:

@Test
public void testUpdateShouldReturn400BadRequest() throws Exception {
	Long userId = 123L;
	String requestURI = END_POINT_PATH + "/" + userId;

	User user = new User().email("david.parker")
						 .firstName("David").lastName("Parker")
						 .password("avid808")
						 .id(userId);

	String requestBody = objectMapper.writeValueAsString(user);

	mockMvc.perform(put(requestURI).contentType("application/json").content(requestBody))
		.andExpect(status().isBadRequest())
		.andDo(print());
}

And in the last unit test for Update User API, we expect the status 200 OK is returned:

@Test
public void testUpdateShouldReturn200OK() throws Exception {
	Long userId = 123L;
	String requestURI = END_POINT_PATH + "/" + userId;

	String email = "david.parker@gmail.com";
	User user = new User().email(email)
						 .firstName("David").lastName("Parker")
						 .password("avid808")
						 .id(userId);

	Mockito.when(service.update(user)).thenReturn(user);

	String requestBody = objectMapper.writeValueAsString(user);

	mockMvc.perform(put(requestURI).contentType("application/json").content(requestBody))
		.andExpect(status().isOk())
		.andExpect(jsonPath("$.email", is(email)))
		.andDo(print());
}

Now you can understand what the code means.


8. Test Delete API

The Delete User API is implemented as follows:

@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable("id") Long id) {
	try {
		service.delete(id);
		return ResponseEntity.noContent().build();

	} catch (UserNotFoundException e) {
		e.printStackTrace();
		return ResponseEntity.notFound().build();
	}
}

You see, this API will return status 404 Not Found if no user found with the given ID, or 204 No Content if a user found and is deleted. Thus we should code 2 unit tests. The first one is as below:

@Test
public void testDeleteShouldReturn404NotFound() throws Exception {
	Long userId = 123L;
	String requestURI = END_POINT_PATH + "/" + userId;

	Mockito.doThrow(UserNotFoundException.class).when(service).delete(userId);;

	mockMvc.perform(delete(requestURI))
		.andExpect(status().isNotFound())
		.andDo(print());
}

Note that, the static method delete() is used to perform an HTTP DELETE request.

And in the second case, we expect the API will return status 204 because we suppose that a user is found with the given ID. Below is the code:

@Test
public void testDeleteShouldReturn200OK() throws Exception {
	Long userId = 123L;
	String requestURI = END_POINT_PATH + "/" + userId;

	Mockito.doNothing().when(service).delete(userId);;

	mockMvc.perform(delete(requestURI))
		.andExpect(status().isNoContent())
		.andDo(print());
}

That’s my tutorial and code examples about unit testing REST APIs with Spring Boot, JUnit, Spring Test and Mockito. I hope you found it helpful. You can download the sample project in the Attachment section below.

Watch the following video to see the coding in action:

 

References:

 

Related REST API Tutorials:


About the Author:

is certified Java programmer (SCJP and SCWCD). He began programming with Java back in the days of Java 1.4 and has been passionate about it ever since. You can connect with him on Facebook and watch his Java videos on YouTube.



Attachments:
Download this file (REST-API-Unit-Testing.zip)REST-API-Unit-Testing.zip[Sample Spring Boot project]88 kB

Add comment