使用 PowerMock 寫單元測試,被坑慘了!
大家好,我是君哥。
最近在工作中遇到一個不太好解決的問題,我負責的系統單元測試跑的非常慢,有時候甚至超過 2 個半小時。
公司要求上線前流水線里面的單測必須全部跑成功。跑流水線的時候如果有單測跑失敗,需要修改后重新跑,又得跑 2 個多小時。極端情況下得反反復復來幾次,真的讓人感到煎熬。有時候發現測試用例跑失敗的原因竟然是 OOM。
今天就來聊一聊造成單測跑的慢的罪魁禍首,PowerMock。
1.PowerMock 基礎
要說 PowerMock 怎么樣,那是真的非常好用。下面列給出幾個示例,先上一段業務代碼,然后我們通過 3 個測試用例把這段代碼單測覆蓋率寫到 100%。
1 public class FileParser {
2
3 private Logger logger = LoggerFactory.getLogger(getClass());
4
5 @Resource
6 private UserRepository userRepository;
7
8 public void parseFile(String fileName) {
9 File file = new File(fileName);
10 if (!file.exists()){
11 return;
12 }
13 try {
14 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
15 String line = null;
16 while ((line = bufferedReader.readLine())!= null){
17 User user = userRepository.getUser(line);
18 logger.info("user with name{}:{}", line, user);
19 }
20 }catch (IOException e){
21 throw new RuntimeException(e);
22 }
23 }
24 }
這段代碼涉及到讀文件、依賴注入、異常處理,我們寫單測也從這三個方面來完成。
1.1 文件不存在
我們先來模擬一下文件不存在,這個用例覆蓋到上面文件不存在的判斷。測試用例如下 :
@Test
public void testParseFile_not_exists() throws Exception {
File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(file);
when(file.exists()).thenReturn(false);
fileParser.parseFile("123");
Mockito.verify(userRepository, Mockito.times(0)).getUser(anyString());
}
這里使用 PowerMock 方便地模擬了第 11 行代碼文件不存在,用例成功。
1.2 循環跳出
這段用例要模擬按行讀文件、dao 層查詢用戶、跳出循環這三個代碼,測試用例代碼如下:
@Test
public void testParseFile_exists() throws Exception {
File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(file);
when(file.exists()).thenReturn(true);
FileInputStream fileInputStream = PowerMockito.mock(FileInputStream.class);
PowerMockito.whenNew(FileInputStream.class).withAnyArguments().thenReturn(fileInputStream);
InputStreamReader inputStreamReader = PowerMockito.mock(InputStreamReader.class);
PowerMockito.whenNew(InputStreamReader.class).withAnyArguments().thenReturn(inputStreamReader);
BufferedReader bufferedReader = PowerMockito.mock(BufferedReader.class);
PowerMockito.whenNew(BufferedReader.class).withAnyArguments().thenReturn(bufferedReader);
//模擬循環和跳出
when(bufferedReader.readLine()).thenReturn("testUser").thenReturn("user").thenReturn(null);
User user = PowerMockito.mock(User.class);
when(userRepository.getUser(anyString())).thenReturn(user);
fileParser.parseFile("123");
Mockito.verify(userRepository, Mockito.times(1)).getUser(anyString());
}
這段用例跑完后,已經覆蓋到源代碼的第 17行和 19 行。
1.3 模擬異常
源代碼中有一個異常處理,用例要達到 100% 覆蓋,必須把這個異常用測試用例模擬出來。下面看一下測試用例:
@Test(expected = RuntimeException.class)
public void testParseFile_exception() throws Exception {
File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(file);
when(file.exists()).thenReturn(true);
FileInputStream fileInputStream = PowerMockito.mock(FileInputStream.class);
PowerMockito.whenNew(FileInputStream.class).withAnyArguments().thenReturn(fileInputStream);
InputStreamReader inputStreamReader = PowerMockito.mock(InputStreamReader.class);
PowerMockito.whenNew(InputStreamReader.class).withAnyArguments().thenReturn(inputStreamReader);
BufferedReader bufferedReader = PowerMockito.mock(BufferedReader.class);
PowerMockito.whenNew(BufferedReader.class).withAnyArguments().thenReturn(bufferedReader);
//模擬拋出異常
when(bufferedReader.readLine()).thenThrow(new IOException());
fileParser.parseFile("123");
}
至此,單測覆蓋率達到 100%。
2.PowerMock 進階
下面再來使用幾個 PowerMock 的功能。再來一段示例代碼:
1 public void parseFileWithScanner(String fileName) {
2 File file = new File(fileName);
3 if (!file.exists()){
4 return;
5 }
6 try {
7 Scanner scanner = new Scanner(file);
8 String line = null;
9 while (scanner.hasNextLine()){
10 line = scanner.nextLine();
11 if (StringUtils.equals(line, "testUser")){
12 User user = userRepository.getUser(line);
13 logger.info("user with name{}:{}", line, user);
14 }
15 }
16 }catch (IOException e){
17 throw new RuntimeException(e);
18 }
19 }
這次我們也要增加 2 個用例的 mock,一個是 Scanner 這個 final 類,第二個是 StringUtils 這個靜態類。
2.1 final 類
雖然是一個 final 類,但使用了 PowerMock 框架,我們就像普通類一樣就可以用例。
@Test
public void testParseFile_scanner() throws Exception {
File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(file);
when(file.exists()).thenReturn(true);
Scanner scanner = PowerMockito.mock(Scanner.class);
PowerMockito.whenNew(Scanner.class).withAnyArguments().thenReturn(scanner);
//模擬循環
when(scanner.hasNextLine()).thenReturn(true).thenReturn(true).thenReturn(false);
when(scanner.nextLine()).thenReturn("testUser").thenReturn("user");
User user = PowerMockito.mock(User.class);
when(userRepository.getUser(anyString())).thenReturn(user);
fileParser.parseFileWithScanner("123");
Mockito.verify(userRepository, Mockito.times(1)).getUser(anyString());
}
除了 final 類,抽象類、接口都可以 mock,確實很方便。
2.2 靜態類
PowerMock 可以方便地模擬靜態類,下面這個測試用例對 StringUtils 這個靜態類進行了 mock,每次 equals 方法都是返回 false。
@Test
public void testParseFile_StringUtils() throws Exception {
File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(file);
when(file.exists()).thenReturn(true);
Scanner scanner = PowerMockito.mock(Scanner.class);
PowerMockito.whenNew(Scanner.class).withAnyArguments().thenReturn(scanner);
//模擬循環
when(scanner.hasNextLine()).thenReturn(true).thenReturn(true).thenReturn(false);
when(scanner.nextLine()).thenReturn("testUser").thenReturn("user");
when(StringUtils.equals(anyString(), anyString())).thenReturn(false).thenReturn(false);
User user = PowerMockito.mock(User.class);
when(userRepository.getUser(anyString())).thenReturn(user);
fileParser.parseFileWithScanner("123");
Mockito.verify(userRepository, Mockito.times(0)).getUser(anyString());
}
因為 equals 方法一直返回 false,所以 getUser 方法沒有執行到,測試用例中 verify getUser 方法被調用 0 次。需要注意的是,模擬靜態類需要在類定義上面加上一個注解,然后對靜態類要做一次 mockStatic。看下面的 @Before 注解。
@RunWith(PowerMockRunner.class)
@PrepareForTest({FileParser.class, StringUtils.class})
public class FileParserTest {
@Before
public void before(){
PowerMockito.mockStatic(StringUtils.class);
}
3.原因分析
PowerMock 因為使用了 @PrepareForTest、@PowerMockIgnore、@SuppressStaticInitialzationFor 這三個注解,這三個注解的參數值不一樣,會導致每個單測類執行的時候不能復用公有類加載器,而是需要創建一個自己獨有的類加載器。這導致類加載過程十分耗時。
在單測類數量比較少的情況下,單測耗時問題是不會出現的,但是如果一個工程中的單測類數據猛增,比如我們的單測類在 600+,問題就暴露出來的。最難的是不太好做優化,因為如果要去掉 PowerMock 框架,要改造的東西太多了。
4.最后
PowerMock 寫單測對開發人員來說確實很方便,但是如果工程中的代碼量比較大,團隊又要求單測覆蓋率高,那單測類的數量確實會很多,最終結果就是單測耗時時間很長。這種情況并不適合使用 PowerMock 框架。
圖片
同時我們也要看到,PowerMock 最近一次核心代碼更新已經是 4 年前了,單測類數據量多導致的內存問題、耗時問題并沒有解決。所以選型的時候一定要慎重。