走进Java接口测试之解决超大文本数据驱动报OOM问题_spring


前言

上篇文章 走进Java接口测试之测试框架TestNG数据驱动(入门篇)阐述测试框架 TestNG 中的一些基本的概念和玩法,本文带着大家来解决一个实际的工程问题。

问题分析

现象

使用文本做数据驱动的时候出现 JVM Heap 区 OOM。 走进Java接口测试之解决超大文本数据驱动报OOM问题_spring_02

原因

核实下 IDEA 的 JVM 参数设置,JVM 最大可用内存为 2G: 走进Java接口测试之解决超大文本数据驱动报OOM问题_ide_03考虑到参数化文件大概有 20 万条记录,判断这是由于程序一次性读取大量的文本数据导致的。

解法

这时候我们想到测试框架 TestNG 有为这种大量数据驱动场景提供解法,即:延迟数据提供者

有的场景我们需要大量参数进行读取,比如参数数据源是 DB,而数据达到百万级,这样测试程序遍历所有数据时,可能就会导致内存溢出。那么我们怎样解决这个问题?当我们获取了一条数据,对它执行测试方法,然后就废弃这个数据对象,再测试下一个。这个原则是延迟初始化,这个思想就是当你真正需要一个对象时才创建它,而不是提前创建它。

具体实现

为了实现这种延迟加载的方法,TestNG 允许我们从数据提供者返回一个 Iterator 对象,而不是一个二维对象数组。

Iterator 是 java.util 包中的一个接口,它的方法签名如下:


1. public interface Iterator<E> {
2. 
3.     boolean hasNext();
4.     E next();
5.     default void remove();
6. 
7.  }

它可以通过 next 调用下一组数据,这样就有机会在最后一刻实例化相应的对象,即刚好在需要在这些参数的测试方法被调用之前。这样的好处是不用把所有的测试数据都加载到内存中,而是需要的时候就读一条。

首先配置 maven 依赖包:


1.    <dependencies>
2.         <dependency>
3.             <groupId>org.springframework.boot</groupId>
4.             <artifactId>spring-boot-starter</artifactId>
5.         </dependency>
6.         <!--引入 testng 测试框架-->
7.         <dependency>
8.             <groupId>org.testng</groupId>
9.             <artifactId>testng</artifactId>
10.             <version>6.14.3</version>
11.             <scope>compile</scope>
12.         </dependency>
13.         <dependency>
14.             <groupId>org.projectlombok</groupId>
15.             <artifactId>lombok</artifactId>
16.             <optional>true</optional>
17.         </dependency>
18.         <dependency>
19.             <groupId>org.springframework.boot</groupId>
20.             <artifactId>spring-boot-starter-test</artifactId>
21.             <scope>test</scope>
22.             <exclusions>
23.                 <exclusion>
24.                     <groupId>org.junit.vintage</groupId>
25.                     <artifactId>junit-vintage-engine</artifactId>
26.                 </exclusion>
27.             </exclusions>
28.         </dependency>
29.     </dependencies>

然后实现 Iterator 接口,用于从文件中读取数据,返回给被测试类:


1. @Slf4j
2. public class TxtIterator implements Iterator<Object[]> {
3. 
4.     /**
5.      *  数据文件
6.      */
7.     File txtFile;
8.     BufferedReader bs;
9.     String currentLine;
10. 
11.     public TxtIterator(File txtFile) throws IOException {
12.         super();
13.         this.txtFile = txtFile;
14.         try {
15.             bs = new BufferedReader(new FileReader(txtFile));
16.         } catch (FileNotFoundException e) {
17.             log.error("文件找不到");
18.             e.printStackTrace();
19.         }
20.         currentLine = bs.readLine();
21.     }
22. 
23.     @Override
24.     public boolean hasNext() {
25.         if (currentLine != null) {
26.             return true;
27.         } else {
28.             return false;
29.         }
30.     }
31. 
32.     @Override
33.     public String[] next() {
34.         String returnLine = currentLine;
35.         try {
36.             currentLine = bs.readLine();
37.         } catch (IOException e) {
38.             e.printStackTrace();
39.         }
40.         return returnLine.split(",");
41.     }
42. 
43.     @Override
44.     public void remove() {
45.         throw new UnsupportedOperationException("remove");
46.     }
47. }

@DataProvider 函数调用:


1.     String filePath; // 文件名
2. 
3.     @Parameters({"filePath"})
4.     @BeforeClass()
5.     public void beforeClass(String filePath) {
6.         log.info("文件路径:[{}]",filePath);
7.         this.filePath =  System.getProperty("user.dir") + "\\" + filePath;;
8.     }
9. 
10. 
11.     @DataProvider(name = "iterator")
12.     public Iterator<Object[]> iteratorDataProvider() throws IOException {
13.         log.info("文件路径:[{}]",filePath);
14.         return new TxtIterator(new File(filePath));
15.     }

@Test 测试运行函数:


1.     @Test(dataProvider = "iterator" ,description = "测试延迟提供数据")
2.     public void testcase2(String username,String password) throws InterruptedException {
3.        log.info(" username = [{}] ,password = [{}]" ,username,password );
4. 
5.        // 休眠2秒
6.        Thread.sleep(2000);
7.     }

整体测试类代码:


1. @SpringBootTest
2. @Slf4j
3. public class DataProviderTest extends AbstractTestNGSpringContextTests {
4. 
5.     String filePath; // 文件名
6. 
7.     @Parameters({"filePath"})
8.     @BeforeClass()
9.     public void beforeClass(String filePath) {
10.         log.info("文件路径:[{}]",filePath);
11.         this.filePath =  System.getProperty("user.dir") + "\\" + filePath;;
12.     }
13. 
14. 
15.     @DataProvider(name = "iterator")
16.     public Iterator<Object[]> iteratorDataProvider() throws IOException {
17.         log.info("文件路径:[{}]",filePath);
18.         return new TxtIterator(new File(filePath));
19.     }
20. 
21.     @Test(dataProvider = "iterator" ,description = "测试延迟提供数据")
22.     public void testcase2(String username,String password) throws InterruptedException {
23.        log.info(" username = [{}] ,password = [{}]" ,username,password );
24. 
25.        // 休眠2秒
26.        Thread.sleep(2000);
27.     }
28. 
29. }

testng.xml


1. <?xml version="1.0" encoding="UTF-8"?>
2. <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
3. <suite name="Suite">
4.     <parameter name="filePath" value="data.txt"/> <!--文件名-->
5.     <test name="DataProviderTest">
6.         <classes>
7.             <class name="com.zuozewei.springboottextdatadrivendemo.TestCase.DataProviderTest" />
8.         </classes>
9.     </test>
10. </suite>

数据文件 data.txt


1. Liming,12
2. HanMeimei,13
3. Lily,11
4. Lucy,12

运行结果: 走进Java接口测试之解决超大文本数据驱动报OOM问题_spring_04

小结

  • 运行 testng.xml,找到对应的测试类,执行前需要初始化 filePath 参数,于是从 testng.xml 文件中把参数的值取出来,传给了测试类中的 filePath 变量。
  • 开始执行测试,发现该测试方法需要一个 DataProvider,于是在本类中找到了 iteratorDataProvider() 方法,执行该方法,构造出 Iterator 对象,传递给测试方法。
  • Iterator 对象使用了 filePath 值构造出一个 BufferedReader 对象,每当测试方法需要一条数据时就由 BufferedReader 读一条数据出来,再拆分成数组,返回给测试方法调用。

这样就实现了延迟提供的数据驱动


示例代码:

https://github.com/7DGroup/Java-API-Test-Examples/tree/master/springboot-text-data-driven-demo