Java 单元测试生成大量 Excel 测试数据实战指南

Java 单元测试生成大量 Excel 测试数据实战指南

一、为什么需要生成测试数据

在开发批量导入功能时,需要验证以下场景:

测试场景 数据量 目的
正常流程 少量(100条) 快速验证基本功能
性能测试 大量(8万~12万) 验证异步插入性能、内存占用
边界测试 恰好上限/超出上限 验证上限校验逻辑
异常数据 含非法值 验证校验逻辑是否正确中断
空数据 只有表头 验证空数据提示

手动在 Excel 中填12万行不现实,用代码生成是唯一合理的方式。


二、技术选型

2.1 为什么用 JUnit 测试类而非 main 方法

方式 优点 缺点
JUnit 测试方法 IDE 一键运行、可选择性执行、不影响生产代码
main 方法 简单 需要手动指定运行类,混在生产代码中
独立脚本 独立 需要单独配置依赖和运行环境

JUnit 测试类放在 src/test/java 下,不会被打包到生产 jar 中,也可以利用项目已有的所有依赖(如 POI)。

2.2 POI 的两种写入模式

模式 内存占用 适用场景
DOM 模式 XSSFWorkbook 全部在内存 数据量 < 50万行
流式模式 SXSSFWorkbook 仅保留窗口行数 数据量 > 50万行

对于12万行数据,XSSFWorkbook 完全够用(内存大约占用 200到500MB),生成时间约 10~~30 秒。如果需要生成百万行以上,应使用 SXSSFWorkbook。

2.3 SXSSFWorkbook vs XSSFWorkbook

java 复制代码
// XSSFWorkbook:所有行都在内存中
XSSFWorkbook workbook = new XSSFWorkbook();  // 适合 < 50万行

// SXSSFWorkbook:只保留最近 N 行在内存,其余刷到磁盘临时文件
SXSSFWorkbook workbook = new SXSSFWorkbook(1000);  // 窗口大小1000行
// 超过1000行的部分自动写入临时文件
// 最终 write() 时合并输出
workbook.dispose(); // 清理临时文件

三、测试数据设计原则

3.1 数据多样性

不要所有行都用同一组值,应准备多组候选值随机选取:

  • 避免数据库唯一约束导致的误判
  • 模拟真实业务数据的分布
  • 测试不同字符长度对性能的影响

3.2 边界值覆盖

字段 正常值 边界值
数量 1~999 0, -1, 999999, 1000000
文本 正常长度 空字符串, 超长字符串, 特殊字符
行数 正常量 0行, 1行, 上限行, 上限+1行

3.3 分层测试文件

为不同测试目的生成不同文件,而非一个文件覆盖所有场景:

复制代码
test_small.xlsx      → 快速冒烟测试(10秒内跑完)
test_normal.xlsx     → 正常业务量测试
test_limit.xlsx      → 上限边界测试
test_over_limit.xlsx → 超出上限测试
test_invalid.xlsx    → 异常数据校验测试
test_empty.xlsx      → 空数据测试

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

四、完整示例代码

以下是一个通用的"订单批量导入测试数据生成器":

4.1 项目依赖

项目中已有 POI 和 JUnit 依赖即可直接使用:

xml 复制代码
<!-- 通常主项目已有 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.0.0</version>
</dependency>

<!-- test scope -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

4.2 测试数据生成器

java 复制代码
package com.example.testdata;

import java.io.FileOutputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;

/**
 * 测试数据Excel生成器.
 *
 * <p>使用方式:在IDE中右键运行对应的测试方法,文件生成到指定路径.
 * 生成的文件用于导入接口的手动测试或自动化测试.
 */
class ImportTestDataGenerator {

  // ==================== 输出路径配置 ====================
  /** 文件输出目录(按需修改). */
  private static final String OUTPUT_DIR = "D:/testdata/";

  // ==================== 候选数据池 ====================
  private static final String[] ORDER_CODES = {
      "ORD20260001", "ORD20260002", "ORD20260003", "ORD20260004", "ORD20260005",
      "ORD20260006", "ORD20260007", "ORD20260008", "ORD20260009", "ORD20260010"
  };

  private static final String[] PRODUCT_CODES = {
      "SKU-10001", "SKU-10002", "SKU-10003", "SKU-10004", "SKU-10005",
      "SKU-20001", "SKU-20002", "SKU-20003", "SKU-20004", "SKU-20005"
  };

  private static final String[] PRODUCT_NAMES = {
      "无线蓝牙耳机", "机械键盘104键", "27寸4K显示器", "静音鼠标",
      "USB-C扩展坞", "笔记本支架", "桌面台灯", "降噪耳机",
      "移动硬盘1TB", "网线6类3米"
  };

  private static final String[] WAREHOUSES = {
      "WH-BJ-01", "WH-SH-01", "WH-GZ-01", "WH-SZ-01", "WH-CD-01"
  };

  private static final String[] WAREHOUSE_NAMES = {
      "北京仓", "上海仓", "广州仓", "深圳仓", "成都仓"
  };

  private static final String[] CATEGORIES = {
      "电子产品", "办公用品", "生活家居", "数码配件", "网络设备"
  };

  /** 表头定义. */
  private static final String[] HEADERS = {
      "订单编号", "商品编码", "商品名称", "分类", "仓库编码", "仓库名称", "数量", "单价"
  };

  // ==================== 测试方法 ====================

  /**
   * 生成少量数据(100条)用于快速验证.
   * 适用场景:开发阶段快速确认导入流程是否通畅.
   */
  @Test
  void generateSmall() throws Exception {
    generateExcel(100, OUTPUT_DIR + "import_small_100.xlsx", DataMode.NORMAL);
  }

  /**
   * 生成中等数据量(1万条).
   * 适用场景:验证分批插入逻辑.
   */
  @Test
  void generateMedium() throws Exception {
    generateExcel(10000, OUTPUT_DIR + "import_medium_10000.xlsx", DataMode.NORMAL);
  }

  /**
   * 生成大数据量(10万条).
   * 适用场景:性能测试,验证异步插入耗时.
   */
  @Test
  void generateLarge() throws Exception {
    generateExcel(100000, OUTPUT_DIR + "import_large_100000.xlsx", DataMode.NORMAL);
  }

  /**
   * 生成上限数据量(12万条).
   * 适用场景:边界测试.
   */
  @Test
  void generateLimit() throws Exception {
    generateExcel(120000, OUTPUT_DIR + "import_limit_120000.xlsx", DataMode.NORMAL);
  }

  /**
   * 生成超出上限(12万+1条).
   * 预期:接口返回"单次导入上限120000条".
   */
  @Test
  void generateOverLimit() throws Exception {
    generateExcel(120001, OUTPUT_DIR + "import_over_limit.xlsx", DataMode.NORMAL);
  }

  /**
   * 生成含异常数据的文件.
   * 预期:接口在遇到第一条异常数据时中断返回.
   */
  @Test
  void generateInvalid() throws Exception {
    generateExcel(200, OUTPUT_DIR + "import_invalid.xlsx", DataMode.INVALID);
  }

  /**
   * 生成空数据文件(只有表头和示例行,没有业务数据).
   * 预期:接口返回"导入数据为空".
   */
  @Test
  void generateEmpty() throws Exception {
    generateExcel(0, OUTPUT_DIR + "import_empty.xlsx", DataMode.NORMAL);
  }

  /**
   * 生成含空行的文件(数据行中夹杂空行).
   * 预期:空行被跳过,其余正常导入.
   */
  @Test
  void generateWithBlankRows() throws Exception {
    generateExcel(100, OUTPUT_DIR + "import_with_blanks.xlsx", DataMode.WITH_BLANK_ROWS);
  }

  /**
   * 生成含特殊字符的文件.
   * 预期:特殊字符正常存储.
   */
  @Test
  void generateSpecialChars() throws Exception {
    generateExcel(50, OUTPUT_DIR + "import_special_chars.xlsx", DataMode.SPECIAL_CHARS);
  }

  // ==================== 核心生成逻辑 ====================

  private enum DataMode {
    NORMAL,          // 全部正常数据
    INVALID,         // 包含非法数据
    WITH_BLANK_ROWS, // 包含空行
    SPECIAL_CHARS    // 包含特殊字符
  }

  private void generateExcel(int dataRows, String filePath, DataMode mode) throws Exception {
    // 确保输出目录存在
    java.io.File dir = new java.io.File(OUTPUT_DIR);
    if (!dir.exists()) {
      dir.mkdirs();
    }

    Random random = new Random(42); // 固定种子,保证每次生成结果一致
    long startTime = System.currentTimeMillis();

    try (XSSFWorkbook workbook = new XSSFWorkbook()) {
      Sheet sheet = workbook.createSheet("导入数据");

      // ---- 创建表头(带样式) ----
      CellStyle headerStyle = createHeaderStyle(workbook);
      Row headerRow = sheet.createRow(0);
      for (int i = 0; i < HEADERS.length; i++) {
        Cell cell = headerRow.createCell(i);
        cell.setCellValue(HEADERS[i]);
        cell.setCellStyle(headerStyle);
        sheet.setColumnWidth(i, 20 * 256); // 列宽20字符
      }

      // ---- 创建示例行 ----
      Row sampleRow = sheet.createRow(1);
      sampleRow.createCell(0).setCellValue("ORD20260001");
      sampleRow.createCell(1).setCellValue("SKU-10001");
      sampleRow.createCell(2).setCellValue("无线蓝牙耳机");
      sampleRow.createCell(3).setCellValue("电子产品");
      sampleRow.createCell(4).setCellValue("WH-BJ-01");
      sampleRow.createCell(5).setCellValue("北京仓");
      sampleRow.createCell(6).setCellValue("10");
      sampleRow.createCell(7).setCellValue("99.90");

      // ---- 创建数据行 ----
      for (int i = 0; i < dataRows; i++) {
        int rowIndex = i + 2; // 从第3行开始

        // 处理空行模式
        if (mode == DataMode.WITH_BLANK_ROWS && i % 20 == 19) {
          sheet.createRow(rowIndex); // 每20行插入一个空行
          continue;
        }

        Row row = sheet.createRow(rowIndex);
        int idx = random.nextInt(10);
        int whIdx = random.nextInt(5);

        row.createCell(0).setCellValue(ORDER_CODES[idx % 10]);

        // 特殊字符模式
        if (mode == DataMode.SPECIAL_CHARS) {
          row.createCell(1).setCellValue("SKU-" + i);
          row.createCell(2).setCellValue("商品【" + i + "】& <测试> \"引号\"");
        } else {
          row.createCell(1).setCellValue(PRODUCT_CODES[idx]);
          row.createCell(2).setCellValue(PRODUCT_NAMES[idx]);
        }

        row.createCell(3).setCellValue(CATEGORIES[whIdx]);
        row.createCell(4).setCellValue(WAREHOUSES[whIdx]);
        row.createCell(5).setCellValue(WAREHOUSE_NAMES[whIdx]);

        // 数量字段(关键校验字段)
        if (mode == DataMode.INVALID) {
          row.createCell(6).setCellValue(getInvalidQuantity(i, random));
        } else {
          row.createCell(6).setCellValue(random.nextInt(999) + 1);
        }

        // 单价
        double price = Math.round((random.nextDouble() * 1000 + 1) * 100.0) / 100.0;
        row.createCell(7).setCellValue(price);
      }

      // ---- 写入文件 ----
      try (FileOutputStream fos = new FileOutputStream(filePath)) {
        workbook.write(fos);
      }
    }

    long cost = System.currentTimeMillis() - startTime;
    java.io.File file = new java.io.File(filePath);
    System.out.println("=== 文件生成完成 ===");
    System.out.println("路径:" + filePath);
    System.out.println("数据行数:" + dataRows);
    System.out.println("文件大小:" + formatFileSize(file.length()));
    System.out.println("生成耗时:" + cost + "ms");
    System.out.println("==================");
  }

  /**
   * 生成非法数量值.
   * 在特定行插入非法数据,其余行正常.
   */
  private int getInvalidQuantity(int rowIndex, Random random) {
    return switch (rowIndex) {
      case 48 -> 0;            // 第50行(索引48+2):数量为0
      case 78 -> -5;           // 第80行:负数
      case 98 -> 1000000;     // 第100行:超过6位(100万)
      default -> random.nextInt(999) + 1;  // 正常值
    };
  }

  /**
   * 创建表头单元格样式.
   */
  private CellStyle createHeaderStyle(XSSFWorkbook workbook) {
    CellStyle style = workbook.createCellStyle();
    Font font = workbook.createFont();
    font.setBold(true);
    font.setFontHeightInPoints((short) 12);
    font.setColor(IndexedColors.WHITE.getIndex());
    style.setFont(font);
    style.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex());
    style.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
    return style;
  }

  /**
   * 格式化文件大小.
   */
  private String formatFileSize(long bytes) {
    if (bytes < 1024) return bytes + " B";
    if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
    return String.format("%.1f MB", bytes / (1024.0 * 1024));
  }
}

五、运行与输出

5.1 在 IDE 中运行

  • IntelliJ IDEA:打开文件 → 点击方法名左侧的绿色三角 → Run
  • 或右键类名 → Run 'ImportTestDataGenerator'(运行所有测试方法)

5.2 控制台输出示例

复制代码
=== 文件生成完成 ===
路径:D:/testdata/import_large_100000.xlsx
数据行数:100000
文件大小:5.3 MB
生成耗时:12450ms
==================

5.3 用 Maven 命令运行

bash 复制代码
# 运行指定测试方法
mvn test -Dtest=ImportTestDataGenerator#generateSmall

# 运行所有生成方法
mvn test -Dtest=ImportTestDataGenerator

六、设计技巧

6.1 固定随机种子

java 复制代码
Random random = new Random(42); // 固定种子

好处:每次运行生成的数据完全一致,方便对比测试结果。如果想每次生成不同数据,去掉参数即可:new Random()

6.2 数据池 + 随机选取

java 复制代码
private static final String[] NAMES = {"商品A", "商品B", "商品C"};
// ...
row.createCell(1).setCellValue(NAMES[random.nextInt(NAMES.length)]);

比拼接"商品1"、"商品2"更贴近真实数据分布。

6.3 异常数据插入特定行

java 复制代码
if (rowIndex == 48) return 0;       // 已知第50行会出错
if (rowIndex == 78) return -5;      // 已知第80行会出错

这样测试时可以精确验证错误提示中的行号是否正确。

6.4 枚举控制生成模式

java 复制代码
private enum DataMode {
  NORMAL, INVALID, WITH_BLANK_ROWS, SPECIAL_CHARS
}

一套核心生成逻辑,通过模式参数控制不同行为,避免代码重复。

6.5 输出目录自动创建

java 复制代码
java.io.File dir = new java.io.File(OUTPUT_DIR);
if (!dir.exists()) dir.mkdirs();

避免因目录不存在导致 FileNotFoundException。


七、性能参考

数据量 XSSFWorkbook 生成耗时 文件大小
100 条 < 1s ~10 KB
1 万条 1~2s ~500 KB
5 万条 5~8s ~2.5 MB
10 万条 10~15s ~5 MB
12 万条 12~20s ~6 MB
50 万条 50~80s ~25 MB
100 万条 建议用 SXSSFWorkbook ~50 MB

八、进阶:自动化测试中的使用

生成的文件不仅可以手动用 Postman 测试,也可以在集成测试中直接使用:

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
class ImportApiIntegrationTest {

  @Autowired
  private MockMvc mockMvc;

  @Test
  void testImportSmallFile() throws Exception {
    // 先生成文件
    byte[] fileBytes = generateInMemory(100);

    MockMultipartFile file = new MockMultipartFile(
        "file", "test.xlsx",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        fileBytes
    );

    mockMvc.perform(multipart("/api/import/import")
            .file(file)
            .param("ownerId", "10001")
            .param("ownerName", "测试用户"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.success").value(true));
  }

  private byte[] generateInMemory(int rows) throws Exception {
    try (XSSFWorkbook wb = new XSSFWorkbook()) {
      // ... 生成逻辑(同上,但写到 ByteArrayOutputStream)
      java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
      wb.write(bos);
      return bos.toByteArray();
    }
  }
}

这样就可以实现全自动的导入流程测试,不依赖外部文件。

相关推荐
io无心1 小时前
基于Image 2的多配件商品图生成技术实现(已开源)
java·image2
逢君学术论文AI写作1 小时前
Java第22课:Servlet获取请求参数+POST请求+表单交互
java·servlet·ai写作
神明不懂浪漫1 小时前
【第二章】Java中的数据类型,运算符与程序逻辑控制
java·开发语言·经验分享·笔记
小马爱打代码1 小时前
Java 开发:过滤器(Filter)与拦截器(Interceptor)深度解析 + CORS 跨域完整解决方案
java
我登哥MVP1 小时前
SpringCloud 核心组件解析:服务熔断和降级
java·spring boot·后端·spring·spring cloud·java-ee·maven
霸道流氓气质2 小时前
Spring AI Alibaba Graph 全解析:从入门到精通
java·人工智能·spring
摇滚侠2 小时前
SpringMVC 入门到实战 异常处理 83-85
java·后端·spring·maven·intellij-idea
Solis程序员2 小时前
长会话状态治理(上):问题分析、存储分层与恢复机制
java