TestNG + PowerMock 單元測試
單元測試(Unit Testing),是指對軟件或項目中最小可測試單元進行正確性檢驗的測試工作。單元是人為規(guī)定最小可測試的功能模塊,可以是一個模塊,一個函數(shù)或者一個類。單元測試需要與模塊開發(fā)進行隔離情況下進行測試。
在程序開發(fā)完成后,我們往往不能保證程序 100% 的正確,通過單元測試的編寫,我們可以通過自動化的測試程序將我們的輸入輸出程序進行定義,通過斷言來 Check 各個 Case 的結果,檢測我們的程序。以提高程序的正確性,穩(wěn)定性,可靠性,節(jié)省程序開發(fā)時間。我們在項目中主要用到的單元測試框架有 Spring-Boot-Test TestNG、PowerMock 等。
TestNG,即 Testing, Next Generation,下一代測試技術,是一套根據(jù) JUnit 和 NUnit 思想而構建的利用注釋來強化測試功能的一個測試框架,即可以用來做單元測試,也可以用來做集成測試。
PowerMock 也是一個單元測試模擬框架,它是在其它單元測試模擬框架的基礎上做出的擴展。通過提供定制的類加載器以及一些字節(jié)碼篡改技巧的應用,PowerMock 現(xiàn)了對靜態(tài)方法、構造方法、私有方法以及 Final 方法的模擬支持,對靜態(tài)初始化過程的移除等強大的功能。
常用注解
1. TestNG 注解
- @BeforeSuite 在該套件的所有測試都運行在注釋的方法之前,僅運行一次
- @AftereSuite 在該套件的所有測試都運行在注釋方法之后,僅運行一次
- @BeforeClass 在調用當前類的第一個測試方法之前運行,注釋方法僅運行一次
- @AftereClass 在調用當前類的第一個測試方法之后運行,注釋方法僅運行一次
- @BeforeMethod 注釋方法將在每個測試方法之前運行
- @AfterMethod 注釋方法將在每個測試方法之后運行
- @BeforeTest 注釋的方法將在屬于test標簽內的類的所有測試方法運行之前運行
- @AfterTest 注釋的方法將在屬于test標簽內的類的所有測試方法運行之后運行
- @DataProvider 標記一種方法來提供測試方法的數(shù)據(jù)。注釋方法必須返回一個Object [] [],其中每個Object []可以被分配給測試方法的參數(shù)列表。要從該DataProvider接收數(shù)據(jù)的@Test方法需要使用與此注釋名稱相等的dataProvider名稱
- @Parameters 描述如何將參數(shù)傳遞給@Test方法 ;適用于 xml 方式的參數(shù)化方式傳值
- @Test 將類或方法標記為測試的一部分,此標記若放在類上,則該類所有公共方法都將被作為測試方法
2. PowerMock 注解
- @Mock 注解實際上是 Mockito.mock() 方法的縮寫,我們只在測試類中使用它;
- @InjectMocks 主動將已存在的 mock 對象注入到 bean 中, 按名稱注入, 但注入失敗不會拋出異常;
- @Spy 封裝一個真實的對象,以便可以像其他 mock 的對象一樣追蹤、設置對象的行為;
示例代碼
1. 添加 pom.xml 依賴
以 Spring-Boot 項目為例,首先我們需要添加 TestNG + ProwerMock 依賴依賴如下:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.testng</groupId>
- <artifactId>testng</artifactId>
- <version>${testng.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-api-mockito2</artifactId>
- <version>${powermock.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-module-junit4</artifactId>
- <version>${powermock.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-module-testng</artifactId>
- <version>${powermock.version}</version>
- <scope>test</scope>
- </dependency>
2. 增加單元測試
增加測試代碼
- import com.test.testng.dto.OrderDto;
- import com.test.testng.dto.UserDto;
- import org.mockito.*;
- import org.powermock.modules.testng.PowerMockTestCase;
- import org.testng.annotations.BeforeMethod;
- import org.testng.annotations.Test;
- import static org.junit.jupiter.api.Assertions.*;
- import static org.mockito.Mockito.when;
- public class OrderServiceTest extends PowerMockTestCase {
- @BeforeMethod
- public void before() {
- MockitoAnnotations.openMocks(this);
- }
- @InjectMocks
- private OrderService orderService;
- @Mock
- private UserService userService;
- // 正常測試
- @Test
- public void testCreateOrder() {
- //1. mock method start
- UserDto userDto = new UserDto();
- userDto.setId(100);
- when(userService.get()).thenReturn(userDto);
- //2. call business method
- OrderDto order = orderService.createOrder(new OrderDto());
- //3. assert
- assertEquals(order.getId(), 100);
- }
- // 異常測試
- @Test
- public void testCreateOrderEx() {
- //1. mock method start
- when(userService.get()).thenThrow(new RuntimeException());
- Exception exception = null;
- try {
- //2. call business method
- orderService.createOrder(new OrderDto());
- } catch (RuntimeException e) {
- exception = e;
- }
- //3. assert
- assertNotNull(exception);
- }
- }
常用 Mock 方式
1. Mock 靜態(tài)方法
- //靜態(tài)方法
- UserDto dto = new UserDto();
- dto.setId(100000);
- PowerMockito.mockStatic(UserService.class);
- PowerMockito.when(UserService.loginStatic()).thenReturn(dto);
- UserDto userDto = UserService.loginStatic();
- assertEquals(100000, userDto.getId().intValue());
2. Mock 私有屬性
- //字段賦值
- ReflectionTestUtils.setField(orderService, "rateLimit", 99);
3. Mock 私有方法
- // 模擬私有方法
- MemberModifier.stub(MemberMatcher.method(UserService.class, "get1")).toReturn(new UserDto());
- // 測試私有方法
- Method method = PowerMockito.method(UserService.class, "get1", Integer.class);
- Object userDto = method.invoke(userService, 1);
- assertTrue(userDto instanceof UserDto);
進階使用
1. 參數(shù)化批量測試
在測試數(shù)據(jù)比較多的時候,我們可以通過 @DataProvider 生成數(shù)據(jù)源,通過 @Test(dataProvider = "xxx") 使用數(shù)據(jù), 如下所示:
- import com.test.testng.BaseTest;
- import com.test.testng.dto.UserDto;
- import org.mockito.InjectMocks;
- import org.testng.annotations.DataProvider;
- import org.testng.annotations.Test;
- import static org.testng.Assert.assertFalse;
- import static org.testng.AssertJUnit.assertTrue;
- public class UserServiceTest2 extends BaseTest {
- @InjectMocks
- private UserService userService;
- // 定義數(shù)據(jù)源
- @DataProvider(name = "test")
- public static Object[][] userList() {
- UserDto dto1 = new UserDto();
- UserDto dto2 = new UserDto();
- dto2.setSex(1);
- UserDto dto3 = new UserDto();
- dto3.setSex(1);
- dto3.setFlag(1);
- UserDto dto4 = new UserDto();
- dto4.setSex(1);
- dto4.setFlag(1);
- dto4.setAge(1);
- return new Object[][] {{dto1, null}, {dto2, null}, {dto3, null}, {dto4, null}};
- }
- // 正確場景
- @Test
- public void testCheckEffectiveUser() {
- UserDto dto = new UserDto();
- dto.setSex(1);
- dto.setFlag(1);
- dto.setAge(18);
- boolean result = userService.checkEffectiveUser(dto);
- assertTrue(result);
- }
- // 錯誤場景
- @Test(dataProvider = "test")
- public void testCheckEffectiveUser(UserDto dto, Object object) {
- boolean result = userService.checkEffectiveUser(dto);
- assertFalse(result);
- }
- }
2. 復雜判斷保證測試覆蓋率
案例:
1.判斷有效用戶: 年齡大于 18 并且 sex = 1 并且 flag = 1
- public boolean checkEffectiveUser(UserDto dto) {
- // 判斷有效用戶: 年齡大于 18 并且 sex = 1 并且 flag = 1
- return Objects.equals(dto.getSex(), 1) &&
- Objects.equals(dto.getFlag(), 1) &&
- dto.getAge() != null && dto.getAge() >= 18;
- }
2.拆分邏輯。將其轉換為最簡單的 if ... else 語句。然后增加的單元測試,如下所示:
- public boolean checkEffectiveUser(UserDto dto) {
- if (!Objects.equals(dto.getSex(), 1)) {
- return false;
- }
- if (!Objects.equals(dto.getFlag(), 1)) {
- return false;
- }
- if (dto.getAge() == null) {
- return false;
- }
- if (dto.getAge() < 18) {
- return false;
- }
- return true;
- }
3.拆分后我們可以看到,咱們只需要 5 條單元測試就能做到全覆蓋。
- public class UserServiceTest extends BaseTest {
- @InjectMocks
- private UserService userService;
- // 覆蓋第一個 return
- @Test
- public void testCheckEffectiveUser_0() {
- UserDto dto =new UserDto();
- boolean result = userService.checkEffectiveUser(dto);
- assertFalse(result);
- }
- // 覆蓋第二個 return
- @Test
- public void testCheckEffectiveUser_1() {
- UserDto dto =new UserDto();
- dto.setSex(1);
- boolean result = userService.checkEffectiveUser(dto);
- assertFalse(result);
- }
- // 覆蓋第三個 return
- @Test
- public void testCheckEffectiveUser_2() {
- UserDto dto =new UserDto();
- dto.setSex(1);
- dto.setFlag(1);
- boolean result = userService.checkEffectiveUser(dto);
- assertFalse(result);
- }
- // 覆蓋第四個 return
- @Test
- public void testCheckEffectiveUser_3() {
- UserDto dto =new UserDto();
- dto.setSex(1);
- dto.setFlag(1);
- dto.setAge(1);
- boolean result = userService.checkEffectiveUser(dto);
- assertFalse(result);
- }
- // 覆蓋第五個 return
- @Test
- public void testCheckEffectiveUser_4() {
- UserDto dto =new UserDto();
- dto.setSex(1);
- dto.setFlag(1);
- dto.setAge(18);
- boolean result = userService.checkEffectiveUser(dto);
- assertTrue(result);
- }
- }
4.單測覆蓋率檢測檢測
3. 通過斷言校驗方法參數(shù)
1.assert:斷言是 java 的一個保留字,用來對程序進行調試,后接邏輯運算表達式,如下:
- int a = 0, b = 1;
- assert a == 0 && b == 0;
- // 使用方法:javac編譯源文件,再 java -ea class文件名即可。
2.在 Spring-Boot 中可以使用 Spring 提供的 Assert 類的方法對前端來的參數(shù)進行校驗,如:
- // 檢查年齡 >= 18 歲
- public boolean checkUserAge(UserDto dto){
- Assert.notNull(dto.getAge(), "用戶年齡不能為空");
- Assert.isTrue(dto.getAge() >= 18, "用戶年齡不能小于 18 歲");
- return Boolean.TRUE;
- }
3.如果是需要轉換為,rest api 返回的統(tǒng)一相應消息,我們可以通過:
- @ControllerAdvice
- public class GlobalExceptionHandler {
- @ResponseBody
- @ExceptionHandler(value = IllegalArgumentException.class)
- public Response<String> handleArgError(IllegalArgumentException e){
- return new Response().failure().message(e.getMessage());
- }
- }
總結
原則上來講,在功能模塊的設計過程中我們應該遵循一下原則(參考 《軟件工程-結構化設計準則》):
- 模塊大小適中
- 合適的系統(tǒng)調用深度
- 多扇入、少扇出(增加復用度, 減少依賴程度)
- 單入口,單出口
- 模塊的作用域,應該在模塊內
- 功能應該可以預測的
- 高內聚,低耦合
- 系統(tǒng)分解有層次
- 較少的數(shù)據(jù)冗余
參考文檔
https://testng.org/doc/
https://github.com/powermock/powermock
https://www.netconcepts.cn/detail-41004.html