- JUnit 是 java單元測試的必備工具
- 只要方法上加上 @Test ,即可生成一個單元測試
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
JUnit 和 Spring Boot 的版本關係
Spring Boot 版本為2.1
- 僅能使用 JUnit4
Spring Boot 版本為2.2、2.3 >需要在 pom.xml 加上額外設定 , 才能禁用 JUnit4
- 能同時使用 JUnit4 和 JUnit5
Spring Boot 版本為2.4
- 僅能使用JUnit5
JUnit5 的用法
@Test 加在方法上 ,可以生成一個單元測試
- @Test 只能在 test 資料夾底下使用 ,可以將該方法變成可執行的 test case (測項)
- 方法為 public void ,並且沒有任何參數
- 方法名稱可以隨便取 ,用來表達這個test case 想測試哪個功能點 ,方法名稱是精華所在
JUnit5
Assert 用法
若是不符合 assert 斷言的預期結果 ,即測試失敗
| Assert 系列用法 | 用途 |
|---|---|
| assertNull(A) | 斷言 A 為 null |
| assertNotNull(A) | 斷言 A 不為 null |
| assertEquals(A ,B) | 斷言 A 和 B 相等 , 會使用 equals() 方法來判斷 |
| assertTrue(A) | 斷言 A 為 true |
| assertFalse(A) | 斷言 A 為 false |
| assertThrows(exception ,method) | 斷言執行 method 時 ,會噴出 exception |
練習

Calculator
package com.example.demo.UnitTest;
public class Calculator {
public int add(int x ,int y){
return x + y;
}
public int divide(int x ,int y){
return x /y;
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
int result = calculator.add(1,2);
System.out.println(result);
}
}
CalculatorTest
單元測試 Calculator 裡面的方法
package com.example.demo.UnitTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
public void add(){
Calculator calculator = new Calculator();
int result = calculator.add(1,2);
assertNotNull(result);
assertTrue(result > 1);
}
@Test
public void divide(){
Calculator calculator = new Calculator();
// calculator.divide 這個方法去除以0 的時候 確實噴出了 ArithmeticException 這個 exception
assertThrows(ArithmeticException.class ,() -> {
calculator.divide(1,0);
});
}
}
assertThrows
所有的 assert 系列方法都可以在最後面加上一個字串的參數去描述這個錯誤的原因是甚麼
但一般來說 非常需要說明的情況才會加上描述
package com.example.demo.UnitTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
public void add(){
Calculator calculator = new Calculator();
int result = calculator.add(1,2);
assertEquals(5 ,result ,"加法有問題");
}
}

JUnit5 其他常用註解
創建一個MyTest 測試 ,注意位置要放在 test 資料夾下

package com.example.demo.UnitTest;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MyTest {
@Test
public void test1(){
System.out.println("執行 test1");
}
@Test
public void test2(){
System.out.println("執行 test2");
}
}

加上 @BeforeEach
package com.example.demo.UnitTest;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MyTest {
@BeforeEach
public void beforeEach(){
System.out.println("執行 @BeforeEaach");
}
@Test
public void test1(){
System.out.println("執行 test1");
}
@Test
public void test2(){
System.out.println("執行 test2");
}
}
console:
加上 @AfterEach
console:
@Before 跟 @After each、all
@BeforeEach、@AfterEach >很常用
- @BeforeEach 在每次@Test 開始前 ,都會執行一次
- @AfterEach 在每次@Test 結束後 ,都會執行一次
@BeforeAll、@AfterAll >不常用 ,注意方法必須為 public static void
- @BeforeAll 在所有@Test 開始前執行一次
- @AfterAll 在所有@Test 結束後執行一次
@BeforeAll、@AfterAll 這個 static 的限制 , 讓他們不能夠去存取到 Spring 容器中的bean ,就不能去對bean 進行一些設定
package com.example.demo.UnitTest;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.*;
public class MyTest {
@BeforeEach
public void beforeEach(){
System.out.println("執行 @BeforeEaach");
}
@AfterEach
public void afterEach(){
System.out.println("執行 @afterEaach");
}
@BeforeAll
public static void beforeAll(){
System.out.println("執行 @BeforeAll");
}
@AfterAll
public static void afterAll(){
System.out.println("執行 @BeforeAll");
}
}
@Disabled、@DisplayNam
@Disabled 忽略該@Test 不執行
@DisplayNam 自定義顯示名稱
package com.example.demo.UnitTest;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Disabled
@Test
public void add(){
Calculator calculator = new Calculator();
int result = calculator.add(1,2);
assertEquals(3 ,result);
}
@DisplayName("測試除法問題")
@Test
public void divide(){
Calculator calculator = new Calculator();
assertThrows(ArithmeticException.class ,() -> {
calculator.divide(1,0);
}); // calculator.divide 這個方法去除以0 的時候 確實噴出了 ArithmeticException 這個 exception
}
}

如何用JUnit5 去測試 Spring Boot 程式
事前準備
application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3308/mytest?serverTimezone=Asia/Taipei&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=ji#@k7au$a83
SQL
create database mytest;
use mytest;
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
score DOUBLE,
graduate BOOLEAN,
create_date TIMESTAMP
);
INSERT INTO student (name, score, graduate, create_date) VALUES ('Amy', 90.3, true, '2021-09-01 10:20:33');
INSERT INTO student (name, score, graduate, create_date) VALUES ('Rom', 34.6, false, '2021-08-10 17:21:14');
INSERT INTO student (name, score, graduate, create_date) VALUES ('Judy', 100.0, true, '2021-09-05 12:19:48');
INSERT INTO student (name, score, graduate, create_date) VALUES ('Mike', 87.2, true, '2021-09-03 15:01:15');
撰寫 Service 層 、Dao層測試
使用JUnit5 測試 Spring Boot 程式
只要在測試的class上加上 @SpringBootTest ,運行單元測試時 , Spring Boot 就會去啟動 Spring 容器 ,創建所有的bean 出來
@SpringBootTest
public class DemoApplicationTests{
@Autowired
private StudentDao studentDao;
@Test
public void getById(){
studentDao.getById(1);
}
}
@SpringBootTest 不只是創建 bean , 所有的 @Configuration 設定也都會被執行 ,效果等同於直接運行起 Spring Boot 程式
單元測試 Dao getById()、deleteById()
DaoImpl 新增一個刪除
package com.example.demo.dao;
import com.example.demo.JPA.Student;
import com.example.demo.StudentRowMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class StudentDaoImpl implements StudentDao{
@Autowired
private NamedParameterJdbcTemplate nameParameterJdbcTemplate;
public Student getById(Integer studentId){
String sqlstr = "select id ,name from student where id = :studentId";
Map<String ,Object> map = new HashMap<>();
map.put("studentId" ,studentId);
List<Student> list = nameParameterJdbcTemplate.query(sqlstr ,map ,new StudentRowMapper());
if(list.size() > 0){
return list.get(0);
}else{
return null;
}
}
public void deleteByID(Integer studentId){
String sqlstr = "delete from student where id = :studentId";
Map<String ,Object> map = new HashMap<>();
map.put("studentId" , studentId);
nameParameterJdbcTemplate.update(sqlstr , map);
}
}
創建一個 Test
package com.example.demo.dao;
import com.example.demo.JPA.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class StudentDaoImplTest {
@Autowired
private StudentDao studentDao; // 注入 studentDao 這個bean
@Test
public void getById(){
Student student = studentDao.getById(1);
assertNotNull(student);
assertEquals("Amy" ,student.getName());
assertEquals(90.3 ,student.getScore());
assertTrue(student.isGraduate());
assertNotNull(student.getCreateDate());
}
@Test
public void deleteById(){
studentDao.deleteByID(1); // 先刪除
Student student = studentDao.getById(1); // 查詢
assertNull(student);
}
}
但當我們單元測試完getById() 、 deleteById() 會發現一個問題就是
deleteById() 刪除了 id=1 ,導致 getById() 測試失敗
這個時候!!
可以用@Transactional
@Test 加上 @Transactional 後
Spring Boot 會去 rollback 這個單元測試裡面所有執行過的操作 ,這個單元測試所改動過的數據 ,都會被恢復原狀
@Transactional (for 單元測試)
用法: 可以加在方法上 ,也可以加在 class 上
用途: 在單元測試結束後 , rollback (回滾) 所有資料庫操作 , 將數據恢復原狀
package com.example.demo.dao;
import com.example.demo.JPA.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.transaction.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class StudentDaoImplTest {
@Autowired
private StudentDao studentDao; // 注入 studentDao 這個bean
@Transactional
@Test
public void deleteById(){
studentDao.deleteByID(1);
Student student = studentDao.getById(1);
assertNull(student);
}
}
單元測試 Dao insert()、update()
在DaoImpl 加上 insert 、update
package com.example.demo.dao;
import com.example.demo.JPA.Student;
import com.example.demo.StudentRowMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class StudentDaoImpl implements StudentDao{
@Autowired
private NamedParameterJdbcTemplate nameParameterJdbcTemplate;
public Student getById(Integer studentId){
String sqlstr = "select id ,name from student where id = :studentId";
Map<String ,Object> map = new HashMap<>();
map.put("studentId" ,studentId);
List<Student> list = nameParameterJdbcTemplate.query(sqlstr ,map ,new StudentRowMapper());
if(list.size() > 0){
return list.get(0);
}else{
return null;
}
}
// 不確定對不對 ,因為單元測試報錯
public Integer insert(Student student){
String sqlstr="insert into student (name ,score ,graduate) value (:studentName ,:studentScore ,:studentGraduate)";
Map<String ,Object> map = new HashMap<>();
map.put("studentName", student.getName());
map.put("studentScore", student.getScore());
map.put("studentGraduate", student.isGraduate());
nameParameterJdbcTemplate.update(sqlstr , map);
return student.getId();
}
public void update(Student student){
Integer studentId = getById(student.getId()).getId();
String sqlstr = "update student set name = :studentName ,score = :studentScore ,graduate = :studentGraduate where id = " + studentId;
Map<String ,Object> map = new HashMap<>();
map.put("studentName", student.getName());
map.put("studentScore", student.getScore());
map.put("studentGraduate", student.isGraduate());
nameParameterJdbcTemplate.update(sqlstr , map);
}
}
Test
package com.example.demo.dao;
import com.example.demo.JPA.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.transaction.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class StudentDaoImplTest {
@Autowired
private StudentDao studentDao; // 注入 studentDao 這個bean
@Transactional
@Test
public void insert(){
Student student = new Student();
student.setName("Kevin");
student.setScore(66.2);
student.setGraduate(true);
Integer studentId = studentDao.insert(student);
Student result = studentDao.getById(studentId);
assertNotNull(result);
assertEquals("Kevin" ,result.getName());
assertEquals(66.2 ,result.getScore());
assertTrue(result.isGraduate());
assertNotNull(result.getCreateDate());
}
@Test
public void update(){
Student student = studentDao.getById(3);
student.setName("John");
studentDao.update(student);
Student result = studentDao.getById(3);
assertNotNull(result);
assertEquals("John" ,result.getName());
}
}
總結
- 在測試用的StudentDaoImplTest class 中加上 @SpringBootTest
- 運行後 Spring Boot Spring 容器跟所有的Bean 都創建出來 ex: Bean StudentDao 和 Bean StudentService
- 使用@Autowired 注入想要測試的bean
- 撰寫單元測試 ,測試該bean 的方法是否運作正常

如何用JUnit5 去測試 Spring Boot 程式 2
Controller 層的測試
ex: 要模擬一個 GET /students/1 請求
@RestController
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/students/{studentId}")
public Student selectId(@PathVariable Integer studentId){
return studentService.getById(studentId);
}
}
目的: 模擬前端的行為 ,測試API 是否運行正確
不能直接注入 bean 來測試 ,需要透過模擬真實的 API call 來測試
要透過模擬一個 http request 來測試 ,而不是直接注入 bean 來測試