Java 單元測驗撰寫完全教程(TestNG + Mockito + Powermock)
本文是筆者自己對單元測驗的理解,由于剛入行,可能理解不深,希望讀者發現錯誤可以幫忙指出,謝謝,
術語表
| 術語 | 解釋 |
|---|---|
| Unit Testing | 簡稱 UT,單元測驗 |
| Stub | 只做引數填充并直接回傳你想要的結果的代碼段(例如函式 int foo(args) return v) |
| Fake | 提供資料的代碼段,由于單元測驗需要資料,因此就需要產生測驗資料的代碼段, |
| Mock | 模擬,mock 可以提供 stub 的能力,同時可以幫你驗證代碼的行為, |
什么是單元測驗?
單元測驗是為了測驗代碼的最小單元而存在的測驗,怎樣的單元算是最小單元則由程式員自己決定,在面向物件語言中,最小單元往往是一個 (public) method,當然,有些人也會把整個類當成最小單元,不過說到底,單元測驗是越簡單越好的,如果一個單元測驗很復雜,那么它很可能是一個集成測驗,當然,為了每個單元測驗都足夠簡單,一般期望每一個 public 方法所做的事情也盡量簡單,這樣也有利于代碼的復用,
為什么要撰寫單元測驗?
起初,我對單元測驗感到疑惑:明明都有集成測驗了,為什么還需要撰寫單元測驗?通過后期對系統的維護,我感受到了單元測驗的幾個優點:
- 幫助程式員更好地理解需求:每次撰寫單元測驗時,我剛好能對代碼做一次 review,同時可以再理一遍思路,查看是否符合需求,整個程序有點像是小黃鴨除錯法,
- 提升代碼的質量:如果單元測驗難以撰寫,一般說明代碼在撰寫層次上存在缺陷,需要進行調整甚至重構,
- 提供代碼級別的檔案:對于未接觸過當前應用的人來說,他們可以通過單元測驗代碼來了解應用的運行方式,
- 方便代碼修改或者重構時的派錯:當你對原本的代碼進行修改的時候,良好的單元測驗可以及時地告訴你哪些修改會影響到原本的邏輯,
單元測驗框架的選擇(Junit4/Junit5/TestNG)
下面的表格列出了不同的功能使用這三個框架時分別需要使用哪些注解:
| 功能 | Junit 4 | TestNG | Junit 5 |
|---|---|---|---|
| 標注為單元測驗方法 | @Test | @Test | @Test |
| 單元測驗類執行前執行的方法 | @BeforeClass | @BeforeClass | @BeforeAll |
| 單元測驗類執行后執行的方法 | @AfterClass | @AfterClass | @AfterAll |
| 每個方法執行前執行的方法 | @Before | @BeforeMethod | @BeforeEach |
| 每個方法執行后執行的方法 | @After | @AfterMethod | @AfterEach |
| 禁用單元測驗 | @Ignore | @Ignore/@Test(enabled=false) | @Disable |
| 斷言例外 | @Test(expected={Exception.class}) | @Test(expectedExceptions={Exception.class}) | @Test(expected={Exception.class}) |
從常用功能上來看,三者實際上都足夠開發使用,但是從以來引進的便攜度來說,TestNG 更加方便,引入一個包就包含了測驗結果報告生產的功能,而 Junit 則需要額外引入生產報告的擴展,在提供的斷言工具類中,TestNG 的 assertEquals(actual, expected) 相比 Junit 的 isEquals(expected, actual) 更符合我的習慣,因此我使用 TestNG 來寫本篇教程,
使用 TestNG
首先,引入 TestNG 的依賴,由于我使用的是 gradle,因此只寫了 gradle 的配置方式,maven 的方式可以參考 Maven Surefire Plugin-Using TestNG
// 使用 gradle 引入 testng 依賴
testImplementation 'org.testng:testng:7.3.0'
// 配置測驗套件
test {
useTestNG()
}
簡單撰寫一個工具類并測驗
// MyUtil.java
import java.util.Arrays;
public class MyUtil {
public static int sum(int[] num) {
return Arrays.stream(num).sum();
}
}
// MyUtilTest.java
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
public class MyUtilTest {
// 使用 @Test 注解來表示該方法為單元測驗方法
@Test
void testSum() {
int[] arr = new int[] {1, 2, 3};
// 斷言結果,確保結果正確
assertEquals(MyUtil.sum(arr), 6);
}
}
撰寫完代碼之后,執行 gradle test 執行測驗,執行完畢之后可以在 build/reports/ 目錄下找到單元測驗報告,
實際業務中撰寫單元測驗會更加復雜,因為每一個方法都可能依賴幾個其他模塊,為了防止其他模塊影響自己模塊的單元測驗,我們就需要 stub,在 Java 中,一般使用 Mockito 框架來提供這種能力,
使用 Mockito
// Gradle 引入 mockito 依賴
// 使用 inline 版本以提供靜態方法和構造器的 mock 支持,
// 注意:inline 版本不能用于安卓開發
testImplementation 'org.mockito:mockito-inline:3.6.28'
使用示例
// 例子寫的比較奇怪,湊合看吧
public String doFilter(String requestUrl) {
if (requestUrl.contains("manage")) {
User user = SessionUtil.getLoginUser();
if (user == null || !user.getUsername().equalsIgnoreCase("admin")) {
return "redirect:401";
}
}
return "success";
}
// 單元測驗撰寫
// 首先撰寫 mockito 單元測驗的基類
import org.mockito.MockitoAnnotations;
import org.testng.annotations.BeforeClass;
public class BaseMockTest {
@BeforeClass
public void initTest() {
// 老版本的 mockito 使用 MockitoAnnotations.initMocks(this);
MockitoAnnotations.openMocks(this);
}
}
// 所有單元測驗繼承這個基類以提供 mock 的能力
import org.mockito.MockedStatic;
import org.testng.annotations.Test;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;
public class MainTest extends BaseMockTest {
@Test
void testDoFilter() {
User user = new User();
user.setUsername("Admin");
AdminFilter adminFilter = spy(AdminFilter.class);
try (MockedStatic<SessionUtil> mockedSessionUtil = mockStatic(SessionUtil.class)) {
// mock SessionUtil.getLoginUser,mock 靜態方法需要 mockito 3.4.0 以上版本
mockedSessionUtil.when(SessionUtil::getLoginUser).thenReturn(user);
assertEquals(adminFilter.doFilter("/manage"), "success");
}
}
}
使用 Powermock
由于 mockito 現在已經支持 mock 靜態方法和構造器(since 3.5.0),powermock 使用場景變少,如果需要 mock private 方法,則可以考慮使用 powermock,或者當你使用低版本的 mockito 時想要 mock 靜態方法和構造器,也可以使用,但是使用起來更為麻煩,
引入 powermock 依賴,注意,需要將 mockito-inline 依賴更改為 mockito-core
// 更改 mockito 依賴
testImplementation 'org.mockito:mockito-core:3.6.28'
// powermock 依賴
testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
// powermock testng 整合依賴
testImplementation 'org.powermock:powermock-module-testng:2.0.9'
更新單元測驗基類
import org.mockito.MockitoAnnotations;
import org.powermock.modules.testng.PowerMockObjectFactory;
import org.testng.IObjectFactory;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.ObjectFactory;
public class BaseMockTest {
@BeforeClass
public void initTest() {
MockitoAnnotations.openMocks(this);
}
// powermock 物件工廠
@ObjectFactory
public IObjectFactory getObjectFactory() {
return new PowerMockObjectFactory();
}
}
使用 powermock 來 mock 靜態物件
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.testng.annotations.Test;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
// 注意此處引入的是 powermock 中的 mockStatic
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.testng.Assert.assertEquals;
@PrepareForTest(SessionUtil.class)
public class MainTest extends BaseMockTest {
@Test
void testDoFilter() {
AdminFilter adminFilter = spy(AdminFilter.class);
User user = new User();
user.setUsername("Admin");
mockStatic(SessionUtil.class);
when(SessionUtil.getLoginUser()).thenReturn(user);
assertEquals(adminFilter.doFilter("/manage"), "success");
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/234131.html
標籤:其他
