大家好,我是君哥。
最近在工作中遇到一个不太好解决的问题,我负责的系统单元测试跑的非常慢,有时候甚至超过 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 年前了,单测类数据量多导致的内存问题、耗时问题并没有解决。所以选型的时候一定要慎重。