I decided to write this article because I noticed to build a robust production application, it's essential to have good test coverage. I do believe that programming is more about the concept than writing code itself.
That's why I will start this tutorial by explaining a bit of what were are trying to achieve and how to do it. So, If you are interested in building a solid unit test foundation for your apps, this tutorial is for you.
Layered Architecture
To pass data through an application, the pattern which is used is the Repository Pattern
.
In this tutorial, you will practice testing the Spring boot Service layer, so before diving into practice, I want to ensure that you understand all the key concepts we will explore. Check the link below for further explanation about this pattern ↓
So the first thing to know is why you should test the Service layer.
You will write tests of the Service Layer because they are superfast and reduce the developer's time. However, while unit testing particular layers, we sometimes have external dependencies.
In this case, you will have to mock them to ensure your application is working and functional without depending on them. This concept is called, Testing in Isolation. If you look up the noun Mock
in the dictionary, you will find that one of the word's definitions is something made as an imitation.
But in programming, It can be defined as :
Mocking is primarily used in unit testing. An object under test may have dependencies on other (complex) objects. To isolate the behavior of the object you want to test, replace the other objects by mocks that simulate the behavior of the real objects.
Now, you have all the key concepts, let's build our demo application and test the service layer.
Further Explanations
Like I said above when it comes to building a REST API with Java Spring boot the most used pattern is certainly the Repository Pattern (RP).
The Repository Pattern is an abstraction of the Data Access Layer.
It hides the details of how the data is processed and saved from the underlying data source. The details of how the data is stored and retrieved is in the respective repository for each entity of the system.
Like being said above, it’s pretty useful for hiding logic behind data, but also makes it easy for unit testing with spring. But imo, I think it's used by many developers teams because it’s simple to implement and maintain.
Now let’s start the practice.
Building the Project
First thing first, you have to set up a new Spring boot project with spring initializr. Here are dependencies used for this demo project in our pom.xml
file
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
The next step will be to create your JPA Entity
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
private int age;
private String gender;
private String address;
}
Create the JPA Repository
@Repository
public interface StudentRepository extends CrudRepository<Student, Integer> {}
And finally, create your Service for interacting with database
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepository repository;
Student saveOneStudent(Student student) {
final var studentToSave = Student.builder()
.name(student.getName())
.age(student.getAge())
.gender(student.getGender())
.address(student.getAddress())
.build();
return this.repository.save(studentToSave);
}
Student findOneStudent(int studentId) {
return this.repository.findById(studentId).orElseThrow(EntityNotFoundException::new);
}
List<Student> findAllStudent() {
return IterableUtils.toList(this.repository.findAll());
}
void deleteOneStudent(int studentId) {
this.repository.deleteById(studentId);
}
}
Testing Service Layer
As we see above, the service class depends on the repository class, but to test our service class, we will use mocks to simulate the behaviour of the real dependency.
Now we will use Mockito, our Mocking Framework to mock the behaviour of our repository layer
@ExtendWith(MockitoExtension.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudentServiceTest {
@InjectMocks
private StudentService service;
@Mock
private StudentRepository repository;
@Test
void should_save_one_student() {
// Arrange
final var studentToSave = Student.builder().name("Mary Jane").age(25).build();
when(repository.save(any(Student.class))).thenReturn(studentToSave);
// Act
final var actual = service.saveOneStudent(new Student());
// Assert
assertThat(actual).usingRecursiveComparison().isEqualTo(studentToSave);
verify(repository, times(1)).save(any(Student.class));
verifyNoMoreInteractions(repository);
}
@Test
void should_find_and_return_one_student() {
// Arrange
final var expectedStudent = Student.builder().name("Jimmy Olsen").age(28).build();
when(repository.findById(anyInt())).thenReturn(Optional.of(expectedStudent));
// Act
final var actual = service.findOneStudent(getRandomInt());
// Assert
assertThat(actual).usingRecursiveComparison().isEqualTo(expectedStudent);
verify(repository, times(1)).findById(anyInt());
verifyNoMoreInteractions(repository);
}
@Test
void should_not_found_a_student_that_doesnt_exists() {
// Arrange
when(repository.findById(anyInt())).thenReturn(Optional.empty());
// Act & Assert
Assertions.assertThrows(EntityNotFoundException.class, () -> service.findOneStudent(getRandomInt()));
verify(repository, times(1)).findById(anyInt());
verifyNoMoreInteractions(repository);
}
@Test
void should_find_and_return_all_student() {
// Arrange
when(repository.findAll()).thenReturn(List.of(new Student(), new Student()));
// Act & Assert
assertThat(service.findAllStudent()).hasSize(2);
verify(repository, times(1)).findAll();
verifyNoMoreInteractions(repository);
}
@Test
void should_delete_one_student() {
// Arrange
doNothing().when(repository).deleteById(anyInt());
// Act & Assert
service.deleteOneStudent(getRandomInt());
verify(repository, times(1)).deleteById(anyInt());
verifyNoMoreInteractions(repository);
}
private int getRandomInt() {
return new Random().ints(1, 10).findFirst().getAsInt();
}
}
@Mock creates a mock implementation for the classes you need.
I know you’re wondering what the hell is Mockito
? And all those Mock methods… I just want you to remember all those key points below :
- Even with a simple class, take the case of some developers in the future could add some weird condition on this method
saveOneStudent
, and as a result, theStudent
is not saved anymore.
If you write a test for the actual method to verify if thesave
of the repository object is called, the developer will be advised about the situation if he makes some change that affects theStudent
save, because of the failure of the unit test. - A mock is used to simulate the behaviour and to verify the interaction between the class you are testing and his dependency (usually called
Stub
). It tests the protocol, meaning you check that the methods on the stub were invoked the right number of times and also in the right order.
Run all the Tests
We can use a maven command to run our tests for the service layer
mvn -Dtest=StudentServiceTest test
If you have the Jacoco plugin (int the plugin section of your pom.xml
file), it will generate code coverage the next time you will launch the test command
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.4</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- attached to Maven test phase -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
To see the report, go to target/site/jacoco/index.html
and open the file to view the coverage
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn • Twitter • GitHub or Medium
Wrap up
Testing is an unavoidable part of the process of developing an application. In this article, you have seen how to test the service layer of your Spring boot app designed with the Repository Pattern.
The source code of this article can be found on my GitHub.
If you are interested in diving deeper in tests, you could check this article below ↓
The best for the last, if you want to solidify your understanding about testing service layer, there are some keywords you can actively recall in your memory:
- Mock
- Service
- Repository
- Test Coverage
- Stub