Spring Boot Testing Tutorial – Part 1, in this article series, we are going to learn about **Unit Testing **Spring Boot application using Junit 5 and we will see how to use Mocking frameworks like Mockito.
This will be the part of the 3 part tutorial series which covers the following topics:
Table of Contents
I am going to explain the above concepts by taking a complete project as an example. I am going to use the Reddit Clone Application which I built using Spring Boot and Angular, you can check out the source code of the tutorial here
I also created a written and video tutorial series where I show you how to build the application step by step, you can check it out here if you are interested.
You can follow along with this tutorial, by downloading the Source Code and starting writing tests with me. I will explain the overall application functionality, as we progress in this tutorial.
You can find the source code which includes Unit Tests at this URL: https://github.com/SaiUpadhyayula/spring-boot-testing-reddit-clone
We are going to write unit tests using the Junit5 library, a popular Unit Testing Library for Java applications, before starting to write the unit tests, let’s discuss What exactly is Unit Testing?
Unit Testing is a practice in the software development process, where you test the functionality of a component (in our case a Java class) in isolation, without depending on any external dependencies.
As I already mentioned before, we are going to use JUNIT 5 for writing Unit Tests in our application. We can install Junit 5 in your project by adding the below maven dependency to the pom.xml file.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
We also need to make sure that the Spring Boot Starter Test dependency is also added to our pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
The example project I linked above already contains the Spring Boot Start Test dependency, but if you check the pom.xml of the spring-boot-starter-test library, you can see that it includes Junit 4 as a transitive dependency.
We can exclude this dependency by adding the below configuration to the spring-boot-starter-test dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
Now we should only have Junit 5 dependency in our classpath.
Here you can find the overview of the application architecture
Spring Boot Application Architecture
We have a 3 Tier Architecture with Controller, Service and Persistence Layer, we are going to cover each layer in our Tutorial Series.
As we are mainly emphasizing the Unit Testing we will take some example classes which are part of the Service Layer.
Now let’s start writing our first unit test.
We will start off with writing Tests for the CommentServiceclass which looks like below:
CommentService.java
package com.programming.techie.springredditclone.service;
import com.programming.techie.springredditclone.dto.CommentsDto;
import com.programming.techie.springredditclone.exceptions.PostNotFoundException;
import com.programming.techie.springredditclone.exceptions.SpringRedditException;
import com.programming.techie.springredditclone.mapper.CommentMapper;
import com.programming.techie.springredditclone.model.Comment;
import com.programming.techie.springredditclone.model.NotificationEmail;
import com.programming.techie.springredditclone.model.Post;
import com.programming.techie.springredditclone.model.User;
import com.programming.techie.springredditclone.repository.CommentRepository;
import com.programming.techie.springredditclone.repository.PostRepository;
import com.programming.techie.springredditclone.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
import static java.util.stream.Collectors.toList;
@Service
@AllArgsConstructor
public class CommentService {
private static final String POST_URL = "";
private final PostRepository postRepository;
private final UserRepository userRepository;
private final AuthService authService;
private final CommentMapper commentMapper;
private final CommentRepository commentRepository;
private final MailContentBuilder mailContentBuilder;
private final MailService mailService;
public void save(CommentsDto commentsDto) {
Post post = postRepository.findById(commentsDto.getPostId())
.orElseThrow(() -> new PostNotFoundException(commentsDto.getPostId().toString()));
Comment comment = commentMapper.map(commentsDto, post, authService.getCurrentUser());
commentRepository.save(comment);
String message = mailContentBuilder.build(authService.getCurrentUser() + " posted a comment on your post." + POST_URL);
sendCommentNotification(message, post.getUser());
}
private void sendCommentNotification(String message, User user) {
mailService.sendMail(new NotificationEmail(user.getUsername() + " Commented on your post", user.getEmail(), message));
}
public List<CommentsDto> getAllCommentsForPost(Long postId) {
Post post = postRepository.findById(postId).orElseThrow(() -> new PostNotFoundException(postId.toString()));
return commentRepository.findByPost(post)
.stream()
.map(commentMapper::mapToDto).collect(toList());
}
public List<CommentsDto> getAllCommentsForUser(String userName) {
User user = userRepository.findByUsername(userName)
.orElseThrow(() -> new UsernameNotFoundException(userName));
return commentRepository.findAllByUser(user)
.stream()
.map(commentMapper::mapToDto)
.collect(toList());
}
public boolean containsSwearWords(String comment) {
if (comment.contains("shit")) {
throw new SpringRedditException("Comments contains unacceptable language");
}
return true;
}
}
This **CommentService **class is communicating with **CommentRepository **and **CommentController **classes which are part of the **Persistence **and **Controller Layer **respectively.
CommentRepository.java
package com.programming.techie.springredditclone.repository;
import com.programming.techie.springredditclone.model.Comment;
import com.programming.techie.springredditclone.model.Post;
import com.programming.techie.springredditclone.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByPost(Post post);
List<Comment> findAllByUser(User user);
}
CommentsController.java
package com.programming.techie.springredditclone.controller;
import com.programming.techie.springredditclone.dto.CommentsDto;
import com.programming.techie.springredditclone.service.CommentService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
@RestController
@RequestMapping("/api/comments/")
@AllArgsConstructor
public class CommentsController {
private final CommentService commentService;
@PostMapping
public ResponseEntity<Void> createComment(@RequestBody CommentsDto commentsDto) {
commentService.save(commentsDto);
return new ResponseEntity<>(CREATED);
}
@GetMapping("/by-post/{postId}")
public ResponseEntity<List<CommentsDto>> getAllCommentsForPost(@PathVariable Long postId) {
return ResponseEntity.status(OK)
.body(commentService.getAllCommentsForPost(postId));
}
@GetMapping("/by-user/{userName}")
public ResponseEntity<List<CommentsDto>> getAllCommentsForUser(@PathVariable String userName){
return ResponseEntity.status(OK)
.body(commentService.getAllCommentsForUser(userName));
}
}
Let’s create a unit test for the CommentService class by creating a class called CommentServiceTest, we will concentrate on writing a Test for the method containsSwearWords(String)
CommentServiceTest.java
package com.programming.techie.springredditclone.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class CommentServiceTest {
@Test
@DisplayName("Test Should Pass When Comment do not Contains Swear Words")
public void shouldNotContainSwearWordsInsideComment() {
CommentService commentService = new CommentService(null, null, null, null, null, null, null);
assertFalse(commentService.containsSwearWords("This is a comment"));
}
}
Let’s understand what is going on in this class:
If you try to run this test, you can see that the Test should pass without any problem. We have our first passing Test
A rule of thumb to remember when testing our code, is to make sure that the test we wrote actually fails when the behavior of the code changes, that is the main reason we are writing tests, to get the feedback immediately when we unintentionally changed the behavior of the method.
Let’s change the logic of the method to return true instead of false when a clean comment is passed in as input.
public boolean containsSwearWords(String comment) {
if (comment.contains("shit")) {
throw new SpringRedditException("Comments contains unacceptable language");
}
return true;
}
And if we run our test again, it should fail.
#spring-boot #testing #junit #mockito