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:

  • Unit Testing with Junit 5 and Mockito
  • Integration Tests using Test Containers
  • Testing REST APIs using MockMvc

Table of Contents

  • Source code for Example Project
    * Source Code with Tests included
  • Unit Testing with Junit 5
  • Overview of Application Architecture
  • Your first Unit Test
  • Testing Negative Case
  • Improved Assertions using AssertJ
  • Mocking the dependencies using Mockito
  • Using @Mock annotations
  • Verifying Method Invocations using Mockito
  • Capturing Method Arguments
  • Improving our Tests by using JUnit Lifecycle Methods
  • What’s next ?

Source code for Example Project

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.

Source Code with Tests included

You can find the source code which includes Unit Tests at this URL: https://github.com/SaiUpadhyayula/spring-boot-testing-reddit-clone

Unit Testing with Junit 5

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.

Overview of Application Architecture

Here you can find the overview of the application architecture

Overview of Application Architecture

Spring Boot Application Architecture

Overview of 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.

Your 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:

  • So as you can see, first, we created a CommentServiceTest class, and inside the class, we declared a method which is annotated with @Test which indicates that the below method is a test.
  • We also have a @DisplayName annotation, this annotation helps us to write a meaningful description for our Test, as you can see with the name of the method shouldNotContainSwearWordsInsideComment() its pretty descriptive, but not easily readable.
  • As we are writing Tests for the CommentService class we need to first instantiate the class and as this is a Unit Test we are supposed to Test this class in isolation, that is the reason why we passed null as Constructor Parameter.
  • Next, we called the containsSwearWords() method and we are doing an assertion whether this method is returning our expected value or not.
  • The assertion we are doing with the help of Junit 5 class called Assertions

If you try to run this test, you can see that the Test should pass without any problem. We have our first passing Test

Test Result for CommentService 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.

Failure Test Result for CommentServiceTest

#spring-boot #testing #junit #mockito

Spring Boot Testing Tutorial - Unit Testing with Junit 5 and Mockito
15.70 GEEK