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. 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
Easily list the terms that appear
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;
}
@Data
does not work, you can add an accessor manually.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.
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 ...
There are several possibilities, but it seems that 2 or 3 methods are easy to do, so this time we will use 3 methods.
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.
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
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.
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