Spring Boot is one of the most used frameworks that is used to develop backend applications. To be sure that everything is working as expected we need to test our application.
Native test automation directly tests the code of the application and it requires knowledge of the framework that the application was developed on.
This blog explains the test automation of Spring Boot application components which was developed for this article. The application and all test automation code can be downloaded from GitHub.
The simple spring boot application registers users that come as RESTful HTTP requests into the database. If everything with the request is OK, then the app returns a successful response. Otherwise, the app will return error messages to the user inside a response body. Test automation will ensure that the applications handle valid and invalid data correctly and everything works fine.
Please note that this application does not cover security and all validation implementations. The aim of this article is just to give an elementary-level Spring Boot test automation tutorial.
Installation
Since the aim of this article is not the installation of Spring Boot, I only provide a list of required tools.
- Java 21: Language of the application.
- PostgreSQL 16: Data is saved in this database.
- Postman (optional): The application to make HTTP requests.
- DBeaver (optional): Open source application to manage database visually.
- IntelliJ IDEA Community Edition (optional): Free IDE that is mostly preferred by Java developers.
In this project, we use Maven, Spring Boot 3.3.0, Jar as packaging, and Java 21.
Packages
To install the required packages you should look to the pom.xml
file and then sync it.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>az.isfan</groupId>
<artifactId>automation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>chat</name>
<description>Spring Boot Test Automation </description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
<version>2.2.224</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Here are the short explanations of why we need all of these packages.
- spring-boot-starter-web: This is needed to make HTTP requests like get and post.
- spring-boot-starter-data-jpa: JPA is a known pack to handle database relations.
- lombok: A useful package to prevent writing constructors, getters, and setters in code for nice and readable code.
- spring-boot-starter-validation: This package is used to validate request data. i.e. is a valid email.
- spring-boot-starter-test: This is needed to implement basic JUnit tests.
- h2: This is used for mocking the database. It simulates a database by using RAM and prevents implementing tests on real databases
To sync Maven right click on any place in the pom.xml
file, go to “Maven”, and press the “Reload project” button. This process will automate the download of required packages to the project.
Configuration
To configure the database the application.properties
file was renamed to application.yml
file.
The application.yml
will give information to the code on how to connect to PostgreSQL.
spring:
datasource:
url: jdbc:postgresql://localhost:5433/automation
username: postgres
password: admin
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
database: postgresql
If you are using the DBeaver application then right-click on the database and click the “Create New Database” button the type database name as automation.
To get the URL and port of the database by using DBeaver click on “postgres” and press the “Edit Connection” button.
ddl-aut
variable with create value will create new tables each time. In other words, when you run the application each time the old values will be deleted. If you want to keep old values in the database then give update or none values to the ddl-aut
variable.
password
and username
values are configured during installing the PostgreSQL.
SpringBootApplication
The root class of the application is annotated by @SpringBootApplication
. This class starts the application context and handles calling other classes automatically.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
I renamed the file name to App.java
file. No method is called inside this file in the scope of this tutorial post.
Run Application
In order to be sure that everything is working you can start to run the application itself. To run the application open the root (App.java
) class and press the green triangle button on IntelliJ IDE.
If there will not be any exception message in the terminal then it is working.
Also, you can test the application in action by using Postman software.
The URL will be localhost:8080/api/v1/user/register. Then select POST, Body, raw, and JSON
format as parameters in Postman.
Write the body of the request as shown below JSON code and then click to “Send” button.
{
"nick-name": "isfzade",
"password": "123456",
"email": "info@isfan.az"
}
The response status will be 201 Created
and the body will be as follows.
{
"errors": null,
"response": {
"id": 1,
"nick-name": "isfzade"
},
"is-successful": true
}
Also, you should see the data on the database.
In order to see the error response write the request body as shown below.
{
"nick-name": "isfzade",
"password": "",
"email": ""
}
The error response status be 400 Bad Request
and the body will be as shown below.
{
"errors": [
{
"code": 1006,
"type": "PASSWORD_EMPTY",
"message": "Password cannot be empty"
},
{
"code": 1002,
"type": "EMAIL_EMPTY",
"message": "Email cannot be empty"
}
],
"response": null,
"is-successful": false
}
Run Tests
The automated tests are located inside the test
folder and have the exact folder/file structure of the application (under the main
folder).
In order to run all tests inside a folder right click and then press “Run Tests”.
Also, it is possible to run a single test. Select any test in the test class and press the green triangle button on the IntelliJ IDE.
The results of the tests will be shown on the “Run” window of the IntelliJ IDE.
Mapper Testing
It is a good idea always to start writing tests for the class with fewer or no injections. To do so a mapper service is the ideal starting point. The mapper is a class to convert some object to another one.
@Service
public class UserMapper {
private Logger logger = LoggerFactory.getLogger(UserMapper.class);
public User toUser(UserRegisterDto userRegisterDto) {
logger.info("toUser: userRegisterDto = {}", userRegisterDto);
var user = User.builder()
.nickName(userRegisterDto.nickName())
.email(userRegisterDto.email())
.password(userRegisterDto.password())
.build();
return user;
}
public UserResponseDto toUserResponseDto(User user) {
logger.info("toUserResponseDto: ");
var userResponseDto = new UserResponseDto(
user.getId(),
user.getNickName()
);
return userResponseDto;
}
public ResponseDto<UserResponseDto> toResponseDto(UserResponseDto userResponseDto) {
logger.info("toResponseDto: userResponseDto = {}", userResponseDto);
var responseDto = new ResponseDto(
true,
null,
userResponseDto
);
return responseDto;
}
}
The User
class keeps the password of the user and when another user requests information about the user we send only limited information.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
private Integer id;
@Column(
name = "nick_name",
unique = true,
nullable = false
)
private String nickName;
@Column(
unique = true,
nullable = false
)
private String email;
@Column(
nullable = false
)
private String password;
}
The mapper converts the User
object to the UserResponseDto
object, which contains only nickname information.
public record UserResponseDto(
Integer id,
@JsonProperty("nick-name")
String nickName
) {}
For the mappers, JUnit5 is the only package that is used as a testing tool. @BeforeEach
and @Test
annotations are coming from JUnit. Since UserMapper
does not needs any injection we initialize it inside the setUp()
method. The setUp()
method will be called when each test starts due to @BeforeEach
annotation.
@BeforeEach
void setUp(){
userMapper = new UserMapper();
}
The aim here is to be sure that the provided test conditions will be satisfied when something changes in the main code.
import az.isfan.automation.user.dto.UserRegisterDto;
import az.isfan.automation.user.dto.UserResponseDto;
import az.isfan.automation.user.models.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UserMapperTest {
private UserMapper userMapper;
private String nickname = "testNickName";
private String password = "testPassword";
private String email = "test@isfan.az";
@BeforeEach
void setUp(){
userMapper = new UserMapper();
}
@Test
public void toUser_convertCorrectly() {
var userDto = new UserRegisterDto(
nickname,
password,
password
);
var user = userMapper.toUser(userDto);
assertNotNull(user, "User cannot be null");
assertNotNull(user.getNickName(), "Nickname is null");
assertNotNull(user.getEmail(), "Email is null");
assertNotNull(user.getPassword(), "Password is null");
assertNull(user.getId(), "Id should be null");
assertEquals(userDto.nickName(), user.getNickName(), "Nickname is incorrect");
assertEquals(userDto.password(), user.getPassword(), "Password is incorrect");
assertEquals(userDto.email(), user.getEmail(), "Email is incorrect");
}
@Test
public void toUserResponseDto_convertCorrectly() {
var user = User.builder()
.nickName(nickname)
.password(password)
.email(email)
.build();
var userResponseDto = userMapper.toUserResponseDto(user);
assertNotNull(userResponseDto.nickName(), "Nickname is null");
assertEquals(user.getNickName(), userResponseDto.nickName(), "Nickname is incorrect");
assertNull(userResponseDto.id(), "Id should be null");
}
@Test
public void toResponseDto_convertCorrectly() {
var userResponseDto = new UserResponseDto(
123,
nickname
);
var responseDto = userMapper.toResponseDto(userResponseDto);
assertNotNull(responseDto.isSuccessful(), "isSuccessful is null");
assertNotNull(responseDto.response(), "response is null");
assertNull(responseDto.errors(), "errors should be null");
assertEquals(true, responseDto.isSuccessful(), "isSuccessful");
assertEquals(userResponseDto.nickName(), responseDto.response().nickName(), "nickName is incorrect");
}
}
The UserMapper
has methods to convert one class or record to another for different purposes. Depending on the purpose usage scenario can be; data to save a database, data that comes from the user, and data to send to the user. The purpose of each object in the registration application is:
User
: The class is directly saved to the database. Containsnickname
,email
, andpassword
information.UserRegisterDto
: The record that the user sends as a request when registered. Containsnickname
,email
, andpassword
informationUserResponseDto
: The record that the server sends inside response. Only contains theid
andnickname
.ResponseDto
: The record that the server sends as a response. Containserrors
,is-successful
, andUserResponseDto
.
assertNotNull
, assertNull
, and assertEquals
are the assertion methods that come with the JUnit package. If the condition is not satisfied then these methods will throw AssertionFailedError
and the test will fail.
assertNotNull(user, "User cannot be null");
assertNull(user.getId(), "Id should be null");
assertEquals(userDto.nickName(), user.getNickName(), "Nickname is incorrect");
Assertion methods are easy to use. The name of assertion methods is self-explanatory. assertNull()
method checks if the variable is null or not. If it will not null then it will throw an exception. The message that is written inside the method will be displayed when the test fails.
assertNull(user.getId(), "Id should be null");
assertEquals()
method asserts if the value is equal to the expected value. The expected value should be written first, then the variable that we want to check, and an optional fail message can be provided.
assertEquals(userDto.nickName(), user.getNickName(), "Nickname is incorrect");
Validator Testing
The validator is the important component of Spring Boot which validates the data that is coming from the user. The Validator
class has capability to check if the value is empty or email is a valid email.
The validation annotations are used inside data that comes from the user.
public record UserRegisterDto(
@JsonProperty("nick-name")
@NotEmpty(message = UserErrorMessages.NICK_NAME_EMPTY)
String nickName,
@NotEmpty(message = UserErrorMessages.PASSWORD_EMPTY)
String password,
@NotEmpty(message = UserErrorMessages.EMAIL_EMPTY)
@Email(message = UserErrorMessages.EMAIL_VALID)
String email
) {}
When user tries to register we want to validate that none of the fields are empty by using @NotEmpty
annotations.
@NotEmpty(message = UserErrorMessages.NICK_NAME_EMPTY)
String nickName,
In addition, we want to be sure the user email will be valid email like info@isfan.az instead of info@isfan by using @Email
annotation.
@Email(message = UserErrorMessages.EMAIL_VALID)
String email
Spring Boot made validation testing very easy by calling only two methods. To call the validator object inside the test class, Validation.buildDefaultValidatorFactory()
, factory.getValidator()
is called before each class.
private Validator validator;
@BeforeEach
void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
this.validator = factory.getValidator();
}
Valid and invalid data scenarios are tested to observe and assert expected reactions. The validate()
method of the Validator
class will return a set of violations if validation fails. If validation passes then the method will return an empty set.
Set<ConstraintViolation<UserRegisterDto>> violations = validator.validate(userRegisterDto);
All validator tests are given as follows.
class RestValidatorTest {
private Validator validator;
private String nickname = "testNickName";
private String password = "testPassword";
private String email = "test@isfan.az";
@BeforeEach
void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
this.validator = factory.getValidator();
}
@Test
public void correctVariables_noViolations() {
var userRegisterDto = new UserRegisterDto(
nickname,
password,
email
);
Set<ConstraintViolation<UserRegisterDto>> violations = validator.validate(userRegisterDto);
assertTrue(violations.isEmpty(), "No violation was expected");
}
@Test
public void allIncorrectVariables_ThreeViolations() {
var userRegisterDto = new UserRegisterDto(
null,
null,
null
);
Set<ConstraintViolation<UserRegisterDto>> violations = validator.validate(userRegisterDto);
assertEquals(3, violations.size(), "3 violations were expected");
}
@Test
public void nullEmail_correctMessage() {
var userRegisterDto = new UserRegisterDto(
nickname,
password,
null
);
Set<ConstraintViolation<UserRegisterDto>> violations = validator.validate(userRegisterDto);
assertEquals(1, violations.size(), "1 violations were expected");
var violation = violations.iterator().next();
assertEquals(UserErrorMessages.EMAIL_EMPTY, violation.getMessage(), "Message was incorrect");
}
@Test
public void nullNickName_correctMessage() {
var userRegisterDto = new UserRegisterDto(
null,
password,
email
);
Set<ConstraintViolation<UserRegisterDto>> violations = validator.validate(userRegisterDto);
assertEquals(1, violations.size(), "1 violations were expected");
var violation = violations.iterator().next();
assertEquals(UserErrorMessages.NICK_NAME_EMPTY, violation.getMessage(), "Message was incorrect");
}
@Test
public void nullPassword_correctMessage() {
var userRegisterDto = new UserRegisterDto(
nickname,
null,
email
);
Set<ConstraintViolation<UserRegisterDto>> violations = validator.validate(userRegisterDto);
assertEquals(1, violations.size(), "1 violations were expected");
var violation = violations.iterator().next();
assertEquals(UserErrorMessages.PASSWORD_EMPTY, violation.getMessage(), "Message was incorrect");
}
@Test
public void invalidEmail_correctMessage() {
var userRegisterDto = new UserRegisterDto(
nickname,
password,
"invalidEmail"
);
Set<ConstraintViolation<UserRegisterDto>> violations = validator.validate(userRegisterDto);
assertEquals(1, violations.size(), "1 violation was expected");
var violation = violations.iterator().next();
assertEquals(UserErrorMessages.EMAIL_VALID, violation.getMessage(), "Message was incorrect");
}
}
Repository Testing
Repository is an interface that extends Spring Boots JpaRepository
which contains predefined methods to save, find, and delete data from the database.
Also, it is possible to write custom methods that use custom SQL queries. getByNickName()
and getByEmail()
methods are artificially created in the scope of this project to find users with different search parameters.
public interface UserRepo extends JpaRepository<User, Integer> {
@Query(value="SELECT u FROM User u WHERE u.nickName=:nick_name")
User getByNickName(@Param("nick_name") String nickName);
@Query(value="SELECT u FROM User u WHERE u.email=:email")
User getByEmail(@Param("email") String email);
}
In reality, it is not logical to make tests on a real database. Since a real database will contain real users’ information testing will add mock users into it and may harm the real users’ information. Due to this reason, Spring Boot provides @DataJpaTest
annotation to create a mock database on memory instead of storage. Any operation in the tests will not effect the real database.
@DataJpaTest
class UserRepoTest {}
We inject UserRepo
class by using @Autowired
annotation.
@Autowired
private UserRepo userRepo;
Whole repository testing will be as given below code.
import az.isfan.automation.user.models.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.dao.DataIntegrityViolationException;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class UserRepoTest {
private String nickname = "testNickName";
private String password = "testPassword";
private String email = "test@isfan.az";
@Autowired
private UserRepo userRepo;
@Test
public void correctVariables_saves() {
User user = User.builder()
.nickName(nickname)
.password(password)
.email(email)
.build();
var savedUser = userRepo.saveAndFlush(user);
assertNotNull(savedUser, "Saved user should not be null");
assertEquals(user.getNickName(), savedUser.getNickName(), "Nickname is incorrect");
assertEquals(user.getEmail(), savedUser.getEmail(), "Email is incorrect");
assertEquals(user.getPassword(), savedUser.getPassword(), "Password is incorrect");
}
@Test
public void getUserByNickname_returnsCorrectUser() {
User user = User.builder()
.nickName(nickname)
.password(password)
.email(email)
.build();
userRepo.saveAndFlush(user);
var userFromDb = userRepo.getByNickName(nickname);
assertNotNull(userFromDb, "Saved user should not be null");
assertEquals(user.getNickName(), userFromDb.getNickName(), "Nickname is incorrect");
}
@Test
public void getUserByEmail_returnsCorrectUser() {
User user = User.builder()
.nickName(nickname)
.password(password)
.email(email)
.build();
userRepo.saveAndFlush(user);
var userFromDb = userRepo.getByEmail(email);
assertNotNull(userFromDb, "Saved user should not be null");
assertEquals(user.getEmail(), userFromDb.getEmail(), "Email is incorrect");
}
@Test
public void getNotExistingUserByNickname_returnsNull() {
var userFromDb = userRepo.getByNickName("incorrectNickname");
assertNull(userFromDb, "No user was expected");
}
@Test
public void getNotExistingUserEmail_returnsNull() {
var userFromDb = userRepo.getByEmail("incorrectEmail");
assertNull(userFromDb, "No user was expected");
}
@Test
public void saveWithDuplicateNickName_throwsException() {
User user1 = User.builder()
.nickName(nickname)
.password(password)
.email(email)
.build();
User user2 = User.builder()
.nickName(nickname)
.password(password)
.email("email")
.build();
assertDuplicateField(user1, user2);
}
@Test
public void saveWithDuplicateEmail_throwsException() {
User user1 = User.builder()
.nickName(nickname)
.password(password)
.email(email)
.build();
User user2 = User.builder()
.nickName("nickname")
.password(password)
.email(email)
.build();
assertDuplicateField(user1, user2);
}
@Test
public void saveWithNullFields_throwsException() {
// Nickname
User user = User.builder()
.nickName(null)
.password(password)
.email(email)
.build();
assertNullField(user);
// Password
user = User.builder()
.nickName(nickname)
.password(null)
.email(email)
.build();
assertNullField(user);
// Email
user = User.builder()
.nickName(nickname)
.password(password)
.email(null)
.build();
assertNullField(user);
}
private void assertDuplicateField(User user1, User user2) {
userRepo.saveAndFlush(user1);
assertThrows(
DataIntegrityViolationException.class,
() -> { userRepo.saveAndFlush(user2); }
);
}
private void assertNullField(User user) {
assertThrows(
DataIntegrityViolationException.class,
() -> { userRepo.saveAndFlush(user); }
);
}
}
By using assertThrows()
it is possible to assert if the method throws an exception.
assertThrows(
DataIntegrityViolationException.class,
() -> { userRepo.saveAndFlush(user); }
);
The private methods that do not have @Test
annotation will not be considered as tests. They are used to decrease the code repetition.
@Test
public void saveWithNullFields_throwsException() {
// Nickname
User user = User.builder()
.nickName(null)
.password(password)
.email(email)
.build();
assertNullField(user);
}
private void assertNullField(User user) {}
saveAndFlush()
is used to get an exception when database validation (unique or nullable fields) is not satisfied. Therefore, in tests, we are not using the save()
method to check the database validation.
userRepo.saveAndFlush(user);
Service Testing
Services are the classes that provide business functionality. Differently than the mapper service, there might be a service between the repository and the REST controller.
UserService
is the class that has the register()
method and this method is responsible for data mapping, validation, and database actions.
@Service
public class UserService {
private Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
public UserService(UserRestValidator userRestValidator, UserRepo userRepo, UserMapper userMapper) {
this.userRestValidator = userRestValidator;
this.userRepo = userRepo;
this.userMapper = userMapper;
}
private final UserRestValidator userRestValidator;
private final UserRepo userRepo;
private final UserMapper userMapper;
public ResponseDto<UserResponseDto> register(
UserRegisterDto userRegisterDto
) throws UserExceptions {
logger.info("register: ");
userRestValidator.validate(userRegisterDto);
var user = userMapper.toUser(userRegisterDto);
var userFromDb = userRepo.save(user);
var userResponseDto = userMapper.toUserResponseDto(userFromDb);
return userMapper.toResponseDto(userResponseDto);
}
}
UserService
requires dependencies inside the constructor and they are injected by using @Autowired
annotation.
In order to initialize UserService
we need to initialize dependent classes too. However, we aim to test the service independently from other objects. We already tested the injected classes in their test classes.
To call the UserService
class inside the test class we put we use the openMocks()
method before each test.
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
In order to inject dependencies into the test class, we use the @Mock
annotation to initialize dependencies and the @InjectMocks
annotation to inject mock dependencies into UserService
object.
@InjectMocks
private UserService userService;
@Mock
private UserRestValidator userRestValidator;
@Mock
private UserRepo userRepo;
@Mock
private UserMapper userMapper;
When the method of a mock class is called it will return a null value since it is an imitation. Therefore, the when()
method of the Mockito
class is used to specify what the mock method should return when it is called.
var userRegisterDto = new UserRegisterDto(
nickname,
password,
email
);
var user = User.builder().build();
when(userMapper.toUser(userRegisterDto)).thenReturn(user);
We manually create data objects and return them through the mock method. The aim here is not to check if the mapper converts data correctly and we already did it inside mapper tests. The aim is to check if the business logic of the service is correct. Therefore, we try to verify if the dependency method is called inside service method.
By using Mockito
‘s times()
method we verify that the toUser()
method is called only once.
verify(userMapper, times(1)).toUser(userRegisterDto);
To assert if the method has never been called we use the never()
method.
verify(userMapper, never()).toUser(userRegisterDto);
All in together the UserServiceTest
class looks as shown below.
import az.isfan.automation.common.dto.ResponseDto;
import az.isfan.automation.user.dto.UserRegisterDto;
import az.isfan.automation.user.dto.UserResponseDto;
import az.isfan.automation.user.enums.UserErrors;
import az.isfan.automation.user.exceptions.UserException;
import az.isfan.automation.user.exceptions.UserExceptions;
import az.isfan.automation.user.models.User;
import az.isfan.automation.user.repos.UserRepo;
import az.isfan.automation.user.validators.UserRestValidator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class UserServiceTest {
private String nickname = "testNickName";
private String password = "testPassword";
private String email = "test@isfan.az";
@InjectMocks
private UserService userService;
@Mock
private UserRestValidator userRestValidator;
@Mock
private UserRepo userRepo;
@Mock
private UserMapper userMapper;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
public void registerWithValidFields_correctMethodsCalled() throws UserExceptions {
var userRegisterDto = new UserRegisterDto(
nickname,
password,
email
);
var user = User.builder().build();
var userResponseDto = new UserResponseDto(0, nickname);
ResponseDto<UserResponseDto> response = new ResponseDto(
true,
null,
userResponseDto
);
when(userMapper.toUser(userRegisterDto)).thenReturn(user);
when(userRepo.save(user)).thenReturn(user);
when(userMapper.toUserResponseDto(user)).thenReturn(userResponseDto);
when(userMapper.toResponseDto(userResponseDto)).thenReturn(response);
var returnedResponse = assertDoesNotThrow(
() -> userService.register(userRegisterDto), "Register should not throw exception"
);
verify(userRestValidator, times(1)).validate(userRegisterDto);
verify(userMapper, times(1)).toUser(userRegisterDto);
verify(userRepo, times(1)).save(user);
verify(userMapper, times(1)).toUserResponseDto(user);
verify(userMapper, times(1)).toResponseDto(userResponseDto);
assertNotNull(returnedResponse, "Response should not be null");
assertEquals(response.response(), returnedResponse.response(), "Returned response is not correct");
}
@Test
public void exceptionThrown_rightMethodsCalled() throws UserExceptions {
var userRegisterDto = new UserRegisterDto(
null,
password,
email
);
var exception = new UserException(
UserErrors.UNKNOWN.code,
UserErrors.UNKNOWN,
"Error Message"
);
var exceptions = new UserExceptions(
new ArrayList(List.of(exception))
);
var user = User.builder().build();
var userResponseDto = new UserResponseDto(0, nickname);
doThrow(exceptions).when(userRestValidator).validate(userRegisterDto);
assertThrows(
UserExceptions.class,
() -> userService.register(userRegisterDto), "Register should throw exception"
);
verify(userRestValidator, times(1)).validate(userRegisterDto);
verify(userMapper, never()).toUser(userRegisterDto);
verify(userRepo, never()).save(user);
verify(userMapper, never()).toUserResponseDto(user);
verify(userMapper, never()).toResponseDto(userResponseDto);
}
}
Since, UserService
‘s register()
method may throw an exception, exceptionThrown_rightMethodsCalled()
also will throw too.
@Test
public void exceptionThrown_rightMethodsCalled() throws UserExceptions {}
However, the class will not throw anything in reality since we handled exception throwing with the assertThrows()
method. Therefore, it is safe to declare that the test class may throw and it will not cause any crash during testing.
assertThrows(
UserExceptions.class,
() -> userService.register(userRegisterDto), "Register should throw exception"
);
The function that throws an exception is the UserRestValidator
‘s validate()
method. That function returns void and throws exceptions when the data is not valid. Since it is the void function we should use doThrow()
instead of when().thenThrow()
.
doThrow(exceptions).when(userRestValidator).validate(userRegisterDto);
Rest Controller Testing
The REST controller is the layer that communicates with a user through HTTP.
In order to mock HTTP requests Spring Boot’s @WebMvcTest
annotation is used. Inside the annotation, we declare our controller class.
@WebMvcTest(controllers = UserController.class)
class UserHttpRequestTest {}
In order to use, the mock post method we inject the MockMvc
object into the test class.
@Autowired
private MockMvc mockMvc;
To convert requests and responses to different types we inject ObjectMapper
.
@Autowired
private ObjectMapper objectMapper;
In order to inject a mock of our own services we use @MockBean
annotation instead of @Mock
.
@MockBean
private UserService userService;
@MockBean
private UserExceptionService userExceptionService;
To make a post request we first write the whole URI (portion after localhost:8080). Then we convert our request object by using the writeValueAsString()
method.
ResultActions response = mockMvc.perform(
post("/api/v1/user/register")
.contentType(MediaType.APPLICATION_JSON)
.content(
objectMapper.writeValueAsString(userRegisterDto)
)
);
We get a response from MockMvc
as a ResultActions
object and it has its assertion methods. By using the andExpect()
method we assert conditions.
The MockMvcResultMatchers
class provides field calling and matching methods. status()
returns the HTTP status (i.e. 201 Created
) and by using the isCreated()
method we are matching the result.
response
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.jsonPath("$.is-successful").value(true))
Also, using the jsonPath()
method we get the JSON field. There should be $. at the beginning of the string, and then we write the field name. The corresponding JSON will look like as shown below.
{
"errors": null,
"response": {
"id": 1,
"nick-name": "isfzade"
},
"is-successful": true
}
If we want to assert a list (i.e. errors
field) then we should write the index in front of the field name.
{
"errors": [
{
"code": 1004,
"type": "PASSWORD_EMPTY",
"message": "Password cannot be empty"
}
],
"response": null,
"is-successful": false
}
.andExpect(MockMvcResultMatchers.jsonPath("$.errors[0].code").value(exception.getCode()))
The whole controller tests will be as shown below code.
import az.isfan.automation.common.dto.ErrorDto;
import az.isfan.automation.common.dto.ResponseDto;
import az.isfan.automation.user.dto.UserRegisterDto;
import az.isfan.automation.user.dto.UserResponseDto;
import az.isfan.automation.user.enums.UserErrors;
import az.isfan.automation.user.exceptions.UserException;
import az.isfan.automation.user.exceptions.UserExceptions;
import az.isfan.automation.user.services.UserExceptionService;
import az.isfan.automation.user.services.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@WebMvcTest(controllers = UserController.class)
class UserHttpRequestTest {
private String nickname = "testNickName";
private String password = "testPassword";
private String email = "test@isfan.az";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
@MockBean
private UserExceptionService userExceptionService;
@Test
public void postRequest_returnsCorrectValue() throws Exception {
var userRegisterDto = new UserRegisterDto(
nickname,
password,
email
);
var userResponseDto = new UserResponseDto(
123,
userRegisterDto.nickName()
);
var responseDto = new ResponseDto<UserResponseDto>(
true,
null,
userResponseDto
);
when(userService.register(userRegisterDto)).thenReturn(responseDto);
ResultActions response = mockMvc.perform(
post("/api/v1/user/register")
.contentType(MediaType.APPLICATION_JSON)
.content(
objectMapper.writeValueAsString(userRegisterDto)
)
);
response
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.jsonPath("$.is-successful").value(true))
.andExpect(MockMvcResultMatchers.jsonPath("$.response").value(Matchers.notNullValue()))
.andExpect(MockMvcResultMatchers.jsonPath("$.response.nick-name").value(nickname))
.andExpect(MockMvcResultMatchers.jsonPath("$.errors").value(Matchers.nullValue()));
}
@Test
public void inValidPostRequest_returnsCorrectValue() throws Exception {
var userRegisterDto = new UserRegisterDto(
null,
password,
email
);
UserException exception = new UserException(
UserErrors.UNKNOWN.code,
UserErrors.UNKNOWN,
"Error Message"
);
UserExceptions exceptions = new UserExceptions(
new ArrayList(List.of(exception))
);
var errorDto = new ErrorDto(
exception.getCode(),
exception.getType().toString(),
exception.getMessage()
);
var errorDtoList = List.of (errorDto);
var responseDto = new ResponseDto<List<ErrorDto>>(
false,
errorDtoList,
null
);
when(userService.register(userRegisterDto)).thenThrow(exceptions);
when(userExceptionService.getErrorResponse(exceptions)).thenReturn(responseDto);
ResultActions response = mockMvc.perform(
post("/api/v1/user/register")
.contentType(MediaType.APPLICATION_JSON)
.content(
objectMapper.writeValueAsString(userRegisterDto)
)
);
response
.andExpect(MockMvcResultMatchers.status().isBadRequest());
response
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.response").value(Matchers.nullValue()))
.andExpect(MockMvcResultMatchers.jsonPath("$.is-successful").value(false))
.andExpect(MockMvcResultMatchers.jsonPath("$.errors").value(Matchers.notNullValue()))
.andExpect(MockMvcResultMatchers.jsonPath("$.errors[0]").value(Matchers.notNullValue()))
.andExpect(MockMvcResultMatchers.jsonPath("$.errors[0].code").value(exception.getCode()))
.andExpect(MockMvcResultMatchers.jsonPath("$.errors[0].type").value(exception.getType().toString()))
.andExpect(MockMvcResultMatchers.jsonPath("$.errors[0].message").value(exception.getMessage()));
}
}