使用 PowerMock 写单元测试,被坑惨了!

开发 前端
PowerMock 写单测对开发人员来说确实很方便,但是如果工程中的代码量比较大,团队又要求单测覆盖率高,那单测类的数量确实会很多,最终结果就是单测耗时时间很长。这种情况并不适合使用 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 年前了,单测类数据量多导致的内存问题、耗时问题并没有解决。所以选型的时候一定要慎重。

责任编辑:武晓燕 来源: 君哥聊技术
相关推荐

2021-05-05 11:38:40

TestNGPowerMock单元测试

2021-03-11 12:33:50

JavaPowerMock技巧

2020-03-20 08:00:32

代码程序员追求

2021-07-16 07:57:35

SpringBootOpenFeign微服务

2017-01-16 12:12:29

单元测试JUnit

2017-01-14 23:26:17

单元测试JUnit测试

2020-09-11 16:00:40

Bash单元测试

2021-10-12 19:16:26

Jest单元测试

2017-01-14 23:42:49

单元测试框架软件测试

2017-03-23 16:02:10

Mock技术单元测试

2023-07-26 08:58:45

Golang单元测试

2013-06-04 09:49:04

Spring单元测试软件测试

2012-05-17 09:09:05

Titanium单元测试

2024-10-16 16:09:32

2021-03-28 23:03:50

Python程序员编码

2010-03-04 15:40:14

Python单元测试

2020-08-18 08:10:02

单元测试Java

2019-01-29 09:00:44

PyHamcrest单元测试框架

2021-06-15 08:08:47

Java单元测试

2023-08-02 13:59:00

GoogleTestCTest单元测试
点赞
收藏

51CTO技术栈公众号