UnitTest with SpringBoot + JUnit + Mockito

I wanted to introduce CI to the Spring Boot project, and when I was researching automated testing, various tool names came out and I was confused, so write a note after organizing. It's an article for beginners of automated testing, so I'm sorry if you lie. Not on purpose.

The final project is here

TL;DR

  1. Run the test class in JUnit
  2. Create a mock with Mockito to replace the dependency
  3. Private methods can be tested with reflection
  4. Hamcrest is a matcher for assertions

table of contents

1. Terminology 2. Creating a sample project 3. Creating unit tests with JUnit 4. Creating tests using mock with Mockito 5. Private method testing 6. Test Result Report 7. Conclusion

1. Terminology

Easily list the terms that appear

2. Create a sample project

Anyway, create a project to test. Anything is fine, so I created it with Spring initializer.

key val
Project Gradle project
Language Java
Spring Boot 2.4.1
Group com.sample
Artifact testsample
Name testsample
Description Sample project
Package name com.sample.testsample
Packaging Jar
Java 11
Dependencies Spring Web
H2 Database
Spring Data JPA
Lombok

Once you have a project, let's create a user registration-like process that is common. The added and changed files are as follows

The directory structure of the src part looks like this

src
├── main
│   ├── java
│   │   └── com
│   │       └── sample
│   │           └── testsample
│   │               ├── TestsampleApplication.java
│   │               ├── UserController.java
│   │               ├── UserEntity.java
│   │               ├── UserRepository.java
│   │               └── UserService.java
│   └── resources
│       ├── application.properties
│       ├── schema.sql
│       ├── static
│       └── templates
└── test
    └── java
        └── com
            └── sample
                └── testsample
                    └── TestsampleApplicationTests.java

File contents

UserController.java


package com.sample.testsample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

@RestController
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping(value="/index", method = RequestMethod.GET)
    public String index() {
        return "This page is user page";
    }

    @RequestMapping(value="/add", method = RequestMethod.POST)
    public String add(@RequestParam String name,
                      @DateTimeFormat(pattern = "yyyy-MM-dd")
                      @RequestParam LocalDate birthday) {
        userService.addUser(name, birthday);
        return "Success!!";
    }
}

UserEntity.java


package com.sample.testsample;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = "user")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private Long age;
}

UserRepository.java


package com.sample.testsample;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}

UserService.java


package com.sample.testsample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

@Service
public class UserService {
    @Autowired
    UserRepository userRepository;

    public void addUser(String name, LocalDate birthday) {
        //Creating a User entity
        UserEntity entity = makeUserEntity(name, birthday);
        //Save
        userRepository.save(entity);
    }

    private UserEntity makeUserEntity(String name, LocalDate birthday) {
        //Creating a User entity
        UserEntity entity = new UserEntity();
        entity.setName(name);
        //Calculation and setting of age
        LocalDate now = LocalDate.now();
        Long age = ChronoUnit.YEARS.between(birthday, now);
        entity.setAge(age);
        return entity;
    }
}

application.properties


spring.datasource.url=jdbc:h2:./test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=testsample
spring.datasource.password=testsample
spring.datasource.sql-script-encoding=UTF-8
spring.datasource.initialization-mode=always
spring.datasource.schema=classpath:schema.sql

schema.sql


DROP TABLE user;
CREATE TABLE user (
  id INTEGER NOT NULL AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL,
  age INTEGER NOT NULL,
  PRIMARY KEY (id)
);

Now that the registration process has been completed, let's create a test next.

3. Create unit tests with JUnit

Test classes are created under the test package using the xxxTests.java naming convention. In this case, the logic is in UserService.java, so we want to test this, so add UserServiceTests.java. Let's start with the calckAge () test, which seems easy to test.

UserServiceTests.java


package com.sample.testsample;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;

public class UserServiceTests {
    @Test
public void Age calculation test() {
        UserService service = new UserService(null);
        LocalDate date = LocalDate.of(2000, 01, 01);
        Long age = service.calcAge(date);
        Assertions.assertEquals(21l, age);
    }
}

As of January 11, 2021, this test succeeds, but there is one problem. The test will fail next year: scream: Let's consider some solutions ...

  1. Add the LocalDate argument to calcAge to calculate the difference
  2. Get the date by using Interface with the configuration that goes through the service to get the date.
  3. Configure to go through the date acquisition service and use the mock to overwrite the value for testing

There are several possibilities, but it seems that 2 or 3 methods are easy to do, so this time we will use 3 methods.

4. Create tests using mock with Mockito

Let's rewrite UserService and UserServiceTests by adding the service DateUtils.java for getting the date.

DateUtils.java


package com.sample.testsample;

import org.springframework.stereotype.Component;

import java.time.LocalDate;

@Component
public class DateUtils {
    public LocalDate getNowDate() {
        return LocalDate.now();
    }
}

UserService.java


 package com.sample.testsample;

 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;

 import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;

 @Service
 public class UserService {
+    private final DateUtils dateUtils;
     private final UserRepository userRepository;

     @Autowired
     public UserService(UserRepository userRepository
+        , DateUtils dateUtils) {
         this.userRepository = userRepository;
+        this.dateUtils = dateUtils;
     }

...

 public Long calcAge(LocalDate birthday) {
-        LocalDate now = LocalDate.now();
-        Long age = ChronoUnit.YEARS.between(birthday, now);
+        Long age = ChronoUnit.YEARS.between(birthday, dateUtils.getNowDate());
         return age;
     }
 }

UserServiceTests.java


 package com.sample.testsample;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import java.time.LocalDate;
 
 public class UserServiceTests {
     @Test
public void Age calculation test() {
+        //Creating a Mock
+        DateUtils dateUtils = Mockito.mock(DateUtils.class);
+        Mockito.when(dateUtils.getNowDate()).thenReturn(LocalDate.of(2021, 1, 11));
 
-        UserService service = new UserService(null);
+        UserService service = new UserService(null, dateUtils);
         LocalDate date = LocalDate.of(2000, 01, 01);
         Long age = service.calcAge(date);
         Assertions.assertEquals(21l, age);
     }
 }

Create a mock with the same methods as DateUtils withMockito.mock (). However, since it is a mock, the called method will return null. So use Mockito.when () and thenReturn () to overwrite the return value of dateUtils.getNowDate (). By doing so, this test can be confirmed to be successful regardless of the current date.

The point is to resolve the dependency of the UserService class by constructor injection. Doing so makes it easier to inject mock and I'm happy: blush:

Now, let's create a test for the UserEntity creation method.

5. Private method testing

UserEntitiy is created bymakeUserEntity (), but the access modifier for this method is private. You can't call it as it is, so let's use reflection to create a test.

Let's add a test case to UserServiceTests.

UserServiceTests.java


 public class UserServiceTests {
 /* write other test cases */
+    @Test
+public void Test for user entity creation() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+        //Creating a Mock
+        DateUtils dateUtils = Mockito.mock(DateUtils.class);
+        Mockito.when(dateUtils.getNowDate()).thenReturn(LocalDate.of(2021, 1, 11));
+        //Get private method with reflection
+        UserService service = new UserService(null, dateUtils);
+        Method method = service.getClass().getDeclaredMethod("makeUserEntity", String.class, LocalDate.class);
+        method.setAccessible(true);
+        UserEntity entity = (UserEntity) method.invoke(service, "Richter", LocalDate.of(2000, 1, 1));
+        //Comparison of results
+        Assertions.assertEquals(null, entity.getId());
+        Assertions.assertEquals("Richter", entity.getName());
+        Assertions.assertEquals(21l, entity.getAge());
+    }
 }

I referred to here for how to test the private method. Great thanks! : kissing_closed_eyes: How to test private method in Junit-Qiita

6. Test result report

Now, since it is a sample, there are only this many test cases, but in the actual system, many more test cases will often be executed. Let's check the test result report to display the test result in an easy-to-read manner. When you run the test <project_root>/build/reports/tests/test/index.html A report of the test results has been created in, so let's check it.

7. Conclusion

That's pretty easy, but I've included testing with JUnit in my SpringBoot application. I hope it helps someone's first step.

Recommended Posts

UnitTest with SpringBoot + JUnit + Mockito
Supports multi-port with SpringBoot
Unit test with Junit.
WebAPI unit test and integration test with SpringBoot + Junit5, 4 patterns
Test Web API with junit
JUnit 5 fails with java.lang.NoSuchMethodError: org.junit.platform.launcher.Launcher.execute
Change the port with SpringBoot
Hello World with SpringBoot / Gradle
Sample code for basic mocking and testing with Mockito 3 + JUnit 5
Java unit tests with Mockito
Validate arguments using ArgumentCaptor with mockito
Implement text link with Springboot + Thymeleaf
Introducing Flyway with SpringBoot + Maven + PostgreSQL
About creating an application with springboot
[Java] Test private methods with JUnit
Test Spring framework controller with Junit
A memorandum when starting new development with IntelliJ + Gradle + SpringBoot + JUnit5 (jupiter)