An application should be tested and validated before being distributed. The purpose of testing is to verify that the application respects the functional and non-functional requirements and detects errors within the application.
TDD: Test-Driven Development
Once the requirements and specifications are validated, a process called Test-Driven Development can begin.
You write the tests FIRST and then develop the code. The tests will be created based on
to the agreed requirements and specifications; initially the tests will fail, we will write code in the application to ensure that
tests are passed. Once the test is passed, we can refactor the code in the application to improve it and launch the test again.
Such tests should be designed by analysts and implemented by developers.
If we notice that tests for a certain specification are difficult to develop, we should consider
the fact that that specification is perhaps not correct or at least ambiguous.
Thanks to the TDD technique, we can identify any problems at an early stage of development. Consider that the effort to solve a problem grows exponentially in proportion to the time it takes to find it.
Unit and Integration Test
A Unit Test verifies the functioning of a small part of the application, as a class method, and is independent and isolated
from the other units of the application. We could think of the individual units as the individual layers of the application.
A good unit test is therefore independent of the whole app infrastructure, like the database type and the other layers.
If the method to test has dependencies with other units, they can be mockated (maybe using libraries like Mockito).
In the example we will do, we will test a Service method, and the test will be independent of both the database and the context of
Spring (so this test works with any framework used for Dependency Injection).
An Integration Test verifies the operation of multiple units of the application. Here the context of the framework used is also used in the test phase.
Example of requirement
From the requirements, we are asked to implement REST APIs of findById and create of a User model.
In particular, in findById, the client must receive a 200 and in the response body the JSON of the User found.
If there is no User with the input id, the client must receive 404 with an empty body.
Moreover the client sees 2 fields of the User: name and address; the name is composed by the last name, a space, and the first name.
The project will have these layers:
- entities which will contain the entity User with name, surname and address
- dtos which will contain the DTO UserDTO that maps the entity User, with the fields name ("surname + first name") and address
- converters which have the responsibility to transform User into UserDTO and vice versa
- repos which will contain a Spring JpaRepository for the entity User
- services which will contain UserService which will trivially retrieve a User from the DB and use the converter to transform it into DTO
- controllers which will contain UserController which will have the responsibility to map REST calls and use the business logic of the Service.
Prerequisites
- Set up a jdk (we will use the version 8, but you can use successive versions).
- Installing maven (https://maven.apache.org/install.html).
First step: let's create the entity and the dto
@Entity
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String surname;
private String address;
public User() {}
public User(String name, String surname, String address) {
this.name = name;
this.surname = surname;
this.address = address;
}
//getter, setter, equals and hashcode
}
@Entity
public class UserDTO {
//surname + name
private String name;
private String address;
public UserDTO() {}
public UserDTO(String name, String address) {
this.name = name;
this.address = address;
}
//getter, setter, equals and hashcode
}
Second step: let's create the converter
@Component
public class UserConverter {
public UserDTO userToUserDTO(User user) {
return new UserDTO(user.getSurname() + " " + user.getName(), user.getAddress());
}
public User userDTOToUser(UserDTO userDTO) {
String[] surnameAndName = userDTO.getName().split(" ");
return new User(surnameAndName[1], surnameAndName[0], userDTO.getAddress());
}
}
Third step: let's create a repository for the User entity
public interface UserRepository extends JpaRepository<User,Long> {
}
Fourth step: let's create the service with the findById method
@Service
public class UserServiceImpl implements UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
private UserRepository userRepository;
private UserConverter userConverter;
public UserServiceImpl(UserRepository userRepository, UserConverter userConverter) {
this.userRepository = userRepository;
this.userConverter = userConverter;
}
@Override
public UserDTO findById(Long id) {
return null;
}
}
As we can see, the method returns null for now. We will implement the functionality after running the test.
Fifth step: let's create the test class to test the findById method of the service
A good test should verify the behavior of a method when valid values are provided in input
and even when invalid values are provided.
The service has a dependency of both the repository and the converter. The implementation of the repository does not interest us so it would be
a good idea mock it. For the converter, we could think of doing the same thing, but since it is a very trivial class,
we could think to use the real class in the test, but without bringing up the context of Spring.
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@Spy
private UserConverter userConverter;
private UserService userService;
@BeforeEach
public void init() {
userService = new UserServiceImpl(userRepository, userConverter);
}
@Test
public void findByIdSuccess() {
User user = new User("Vincenzo", "Racca", "via Roma");
user.setId(1L);
when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));
UserDTO userDTO = userService.findById(1L);
verify(userRepository, times(1)).findById(anyLong());
assertNotNull(userDTO);
String[] surnameAndName = userDTO.getName().split( " ");
assertEquals(2, surnameAndName.length);
assertEquals(user.getSurname(), surnameAndName[0]);
assertEquals(user.getName(), surnameAndName[1]);
assertEquals(user.getAddress(), userDTO.getAddress());
}
}
Let's analyze the code:
- @ExtendWith(MockitoExtension.class) allows us to use the mockato context of the Mockito library. Also @ExtendWith
corresponds to @RunWith of JUnit 4. 2) We annotate with @Mock the repository because we want Mockito to create a mockata implementation of the interface. 3) We annotate with @Spy the converter to indicate to Mockito that we want to use the real class. 4) We annotate with @BeforeEach the init method that initializes the userService every time a test method is run. Since we used the constructor dependency injection and not the field injection, we just need to pass in the constructor the mockato repository for create the service. @BeforeEach corresponds to the @Before annotation of JUnit 4.
Let's analyze the test:
We expect that for any input id, the repository will return a User with name Vincenzo, surname Racca and address via Roma.
To do this, we use Mockito's static method when along with anyLong (to indicate any id) and thenReturn to indicate the return value we expect from the method.
When we call the service, we expect a UserDTO with name Racca Vincenzo and address via Roma.\ to return.
With verify we verify that the service calls 1 time the findById of the repository. Then follow various trivial assertions.
We perform the test: it already fails at the call verify because the service method has never called the repository;
Sixth step: let's fix the method
@Override
public UserDTO findById(Long id) {
User user = userRepository.findById(id).get();
return userConverter.userToUserDTO(user);
}
The test now passes, we can evaluate to improve the code without forgetting to re-test the method.
Step 7: let's test the method with an invalid input
Let's create another test case in which the client passes an id of a non-existing User. Backend side if the User will not be found, we will launch an exception to make it easier to read the logs:
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User with id " + id + " not found!");
}
}
The test:
@Test
public void findByIdUnSuccess() {
when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
UserNotFoundException exp = assertThrows(UserNotFoundException.class, () -> userService.findById(1L));
assertEquals("User with id 1 not found!", exp.getMessage());
}
Very trivially we tell Mockito that for any id, the repository will not find any User.
With assertThrows we return the exception we expect to be launched by the service, and then we write an assert on the exception message.
This kind of test is possible with JUnit 5. With JUnit 4 in fact we could verify that the method launched the exception,
but you could not go ahead with the assertions once the error was launched.
Obviously the test fails, so let's fix the method.
@Test
@Override
public UserDTO findById(Long id) {
Optional<User> user = userRepository.findById(id);
if(user.isPresent()) {
return userConverter.userToUserDTO(user.get());
}
else {
throw new UserNotFoundException(id);
}
}
The test now passes
Eighth step: refactoring the method
Since we use java 8, we can refactor the method using streams. We also add a log.
@Override
public UserDTO findById(Long id) {
User user = userRepository.findById(id).orElseThrow(() -> {
UserNotFoundException exp = new UserNotFoundException(id);
LOGGER.error("Exception is UserServiceImpl.findById", exp);
return exp;
});
return userConverter.userToUserDTO(user);
}
Let's retest the method: the test is still green, we can conclude this test case.
Conclusions
We have briefly seen how Test-Driven Development works by developing Unit Tests with JUnit 5 and with the help of Mockito to mock-up the units that are not of interest to the test. In the next article we will continue the development of the requirement by creating a Controller and we will write an Integration Test using Spring Boot, JUnit 5 and H2 as in-memory database.
You can download the full project with also create test from my github in this link: Spring Test
Next article about Integration Tests: TDD e Integration Test
Posts of Spring Framework: Spring
Recommended books about Spring:
- Pro Spring 5 (Spring from scratch a hero): https://amzn.to/3KvfWWO
- Pivotal Certified Professional Core Spring 5 Developer Exam: A Study Guide Using Spring Framework 5 (for Spring certification): https://amzn.to/3KxbJSC
- Pro Spring Boot 2: An Authoritative Guide to Building Microservices, Web and Enterprise Applications, and Best Practices (Spring Boot of the detail): https://amzn.to/3TrIZic