ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java PDF自动化测试实战:用PDFBox与Maven构建可靠断言

Java PDF自动化测试实战:用PDFBox与Maven构建可靠断言 如果你的项目里每天都会生成大量 PDF 文件比如电子合同、对账单、业务报表那么“PDF 能不能测、怎么测、用什么工具去断言 PDF 内容是否正确”迟早会找上门。很多团队对 PDF 相关代码都是“写完就不管”一旦模板调整、字体变更、字段改位线上就会出现格式错乱或者内容丢失而人工核对 PDF 既慢又容易漏。这篇文章就以“Tests for a PDF”为主线讲清楚在 Java 技术栈下如何对 PDF 做自动化测试。我们会从 PDF 测试的难点说起再落到 PDFBox 这个最常用的开源库给出可以直接复制运行的单元测试、集成测试和 CI 集成方式。文章还会专门讨论一个让很多人头疼的问题测试类写好了运行 Maven 时却提示no tests found for given includes这种报错到底是怎么来的又该怎么解决。如果你正在做文档生成、电子签章、报表导出这类业务或者刚接触 PDF 自动化测试这篇文章值得读完再收藏。1. PDF 测试为什么这么麻烦先看一个非常常见的场景。业务系统里一个导出功能后端用模板引擎生成一份产品报价单 PDF。测试人员拿到手之后只能通过肉眼确认“价格对不对、表格有没有换行、公司 Logo 有没有变形”。如果产品上线后模板改了字段测试就要重新人工核对一遍工作量不小但覆盖的基本是“打开文件看一眼”这种最浅层的验证。问题的根源在于PDF 和普通文本文件、JSON、XML 完全不同。它本质上是页面描述语言保存的是文字、图形、字体、坐标、图像等内容的位置关系。你看到的一段“文字”在 PDF 内部可能是一条文本绘制指令也可能被转成了曲线轮廓。不同字体、不同 PDF 生成器、不同渲染引擎都会影响最终排版效果。这导致我们不能像断言接口返回值那样直接用字符串匹配来判断 PDF 内容是否正确。但 PDF 测试也并不是无解。常见的做法是分三层验证内容层校验 PDF 里的文字、表格数据、标题、页数是否完整。这是最常见、覆盖业务最多的层。结构层校验 PDF 的元数据、书签、页面大小、加密属性、字体嵌入情况。视觉层把 PDF 渲染成图片再和基准图片做像素级对比。这个适合验证模板改动引起的版式变化成本比内容层高。大多数业务项目先把内容层和结构层做好基本就能拦住 80% 的回归问题。这也是本文要重点演示的方向。2. 核心概念PDF 文本提取、页面对象与坐标体系在写测试代码之前先花几分钟理清几个关键概念。这些概念决定了我们做断言时的思路。2.1 文本提取PDFBox 提供的PDFTextStripper类可以把整个 PDF 的文本内容抽取出来。抽取结果是按照页面和内容流顺序拼接出来的字符串不是严格的表格结构。这意味着如果你的 PDF 里有一段“客户名称张三”在抽取出的文本里可能表现为“客户名称张三”也可能因为文本框坐标问题变成“客户名称 张三”。所以写断言时不建议直接做全等字符串匹配更稳妥的做法是使用contains或者先对空白做归一化处理。2.2 页面对象与页面树PDF 的每个页面在内部是由页面对象构成的页面对象记录了页面尺寸、资源引用、内容流等属性。PDFBox 里可以通过PDDocument拿到所有页面并读取每个页面的尺寸、旋转角度、备注等。测试中经常用页面对象来断言“PDF 一共应该有几页”“页面尺寸是否符合模板要求”。2.3 坐标体系PDF 的坐标原点在页面左下角x 轴向右y 轴向上单位是 point1 point 1/72 英寸。这和很多前端技术的坐标体系不一样。如果要做更精细的断言比如“某个关键词出现在页面左上角区域”就需要先把 PDF 坐标转换成视觉坐标来理解。对于第一阶段的测试实践建议先做文本级断言等真正遇到排版回归问题再引入区域坐标断言和视觉对比。2.4 元数据与加密属性PDF 不仅可以包含正文还能携带标题、作者、创建时间、关键词等元数据。加密的 PDF 还有文件权限属性比如是否允许打印、是否允许复制。这些都可以通过测试断言尤其是合同类、合规类文件加密权限往往直接关系到业务安全要求。3. 技术选型为什么优先用 Apache PDFBox市面上可以处理 PDF 的 Java 库不少有 iText、Apache PDFBox、OpenPDF、Spire.PDF 等。从测试场景出发最推荐 Apache PDFBox。原因有三个第一Apache PDFBox 是 Apache 顶级开源项目Apache 2.0 许可证商用使用没有版权风险不需要像 iText 那样区分商业版和 AGPL 社区版。第二PDFBox 的 API 以读取和内容提取见长非常适合测试断言。它提供非常直接的文本抽取、页面属性读取、元数据读取能力写测试时思路非常顺。第三社区成熟遇到问题基本都能在 Stack Overflow 和官方文档里找到答案。当然PDFBox 不是万能的。它在复杂表格的重建、文本排序方面比较弱也不擅长把抽取出的文本恢复成单元格结构。如果要用测试去校验非常复杂的表格数据建议结合第三方布局分析逻辑或者直接走视觉对比路线。对于“字段是否齐全、页码是否正常、数据是否被正确写入”这类断言PDFBox 足够。下表对比了一些常见选择库开源协议文本提取能力复杂表格支持适合测试场景Apache PDFBoxApache 2.0好弱文本、元数据、页面属性断言iText 社区版AGPL好中需要注意许可证OpenPDFLGPL/MPL中弱偏生成场景视觉对比工具如 Percy商业/SaaS不适用不适用像素级回归测试4. 环境准备Maven 依赖与目录规划本文示例使用 Java Maven JUnit 5这是 CSDN 读者最熟悉的组合之一。在开始写测试代码前先把依赖和目录准备好。4.1 创建 Maven 项目并添加依赖在pom.xml中加入以下依赖。版本号这里不写死建议以 Maven 中央仓库最新稳定版为准。本文示例基于 PDFBox 2.x 系列的 API2.x 和 3.x 之间 API 有少量调整但核心的PDDocument、PDFTextStripper用法保持一致。properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Apache PDFBox -- dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.30/version /dependency !-- JUnit 5 -- dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.10.2/version scopetest/scope /dependency !-- 用于断言可选 -- dependency groupIdorg.assertj/groupId artifactIdassertj-core/artifactId version3.25.3/version scopetest/scope /dependency /dependency build plugins !-- 强制 Maven 使用 JUnit 5 运行测试 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.2.5/version /plugin /plugins /build如果项目里还没有 PDF 生成逻辑只是为了快速验证测试框架可以用一个已有 PDF 文件放到src/test/resources/samples/目录下。本文示例会尽量做到不依赖外部样例文件通过代码在测试中临时生成一个最小 PDF这样读者复制后可以直接运行。4.2 目录规划实际项目中建议把 PDF 测试相关的工具类放在一个独立的包下src/test/java/com/example/pdf/ ├── PdfTestBase.java # PDF 测试基类负责加载文件 ├── PdfTextAssertionTest.java # 文本断言测试 ├── PdfMetadataAssertionTest.java # 元数据与加密断言测试 └── PdfPageAssertionTest.java # 页面对象断言测试把测试工具和业务测试拆开后续维护会更清晰。5. 完整示例PDF 自动化测试的核心代码这一节直接进入实操。为了让示例可以在本地直接跑通我们不依赖外部 PDF 文件而是先用 PDFBox 在测试中动态生成一个包含固定文本和元数据的 PDF再对它做断言。这样一篇文章就能形成一个闭环。5.1 用 PDFBox 生成最小测试文件下面的工具方法可以生成一个固定内容的 PDF并保存到临时目录。这个方法放在src/test/java/com/example/pdf/PdfTestBase.java中package com.example.pdf; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import java.io.File; import java.io.IOException; import java.util.Calendar; public class PdfTestBase { protected static File createSamplePdf() throws IOException { File pdfFile File.createTempFile(sample-, .pdf); try (PDDocument document new PDDocument()) { PDPage page new PDPage(PDRectangle.A4); document.addPage(page); try (PDPageContentStream cs new PDPageContentStream(document, page)) { cs.beginText(); cs.setFont(PDType1Font.HELVETICA, 14); cs.newLineAtOffset(100, 700); cs.showText(Customer Name: Zhang San); cs.newLineAtOffset(0, -30); cs.showText(Total Amount: 1999.00); cs.endText(); } PDDocumentInformation info new PDDocumentInformation(); info.setTitle(Order Confirmation); info.setAuthor(CSDN Demo); info.setSubject(PDF Test Sample); info.setCreationDate(Calendar.getInstance()); document.setDocumentInformation(info); document.save(pdfFile); } return pdfFile; } }这里的关键点是PDType1Font.HELVETICA是 PDF 标准内置字体不需要额外处理字体文件。PDPageContentStream负责写入内容流。PDDocumentInformation用于设置元数据。使用 try-with-resources 确保PDDocument被正确关闭。5.2 文本内容断言测试下面的测试类演示了如何对 PDF 做文本内容断言package com.example.pdf; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.io.File; import static org.junit.jupiter.api.Assertions.*; public class PdfTextAssertionTest extends PdfTestBase { private static File pdfFile; BeforeAll static void setUp() throws Exception { pdfFile createSamplePdf(); } Test void shouldContainCustomerName() throws Exception { try (PDDocument document PDDocument.load(pdfFile)) { PDFTextStripper stripper new PDFTextStripper(); String content stripper.getText(document); assertTrue(content.contains(Zhang San), PDF 中应包含客户名称); assertTrue(content.contains(1999.00), PDF 中应包含订单金额); } } Test void shouldHaveOnePage() throws Exception { try (PDDocument document PDDocument.load(pdfFile)) { assertEquals(1, document.getNumberOfPages()); } } }运行mvn test后如果一切正常会看到两个测试全部通过。这段测试的核心逻辑非常简单用PDDocument.load从文件加载 PDF用PDFTextStripper抽取文本再对抽取结果做断言。这里要提醒一个常见的坑PDFTextStripper抽取出的文本可能带有大量换行和空格尤其 PDF 是从 HTML 或 Word 转换过来的时候。不要直接做精确相等断言正常情况下应该用contains或者先对文本做空白归一化String normalized content.replaceAll(\\s, ); assertTrue(normalized.contains(Customer Name: Zhang San));5.3 元数据与加密属性断言业务系统导出的合同、发票往往对元数据和权限有要求。比如电子发票 PDF 可能要求标题正确加密文件可能要求禁止打印。我们可以通过下面的断言来覆盖package com.example.pdf; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.io.File; import static org.junit.jupiter.api.Assertions.*; public class PdfMetadataAssertionTest extends PdfTestBase { private static File pdfFile; BeforeAll static void setUp() throws Exception { pdfFile createSamplePdf(); } Test void shouldHaveCorrectTitle() throws Exception { try (PDDocument document PDDocument.load(pdfFile)) { PDDocumentInformation info document.getDocumentInformation(); assertEquals(Order Confirmation, info.getTitle()); assertEquals(CSDN Demo, info.getAuthor()); } } Test void shouldNotBeEncrypted() throws Exception { try (PDDocument document PDDocument.load(pdfFile)) { assertFalse(document.isEncrypted(), 示例 PDF 不应加密); } } }PDDocument提供了isEncrypted()方法来判断文件是否加密。对于权限控制细节比如“是否允许打印”需要先获取访问权限对象if (document.isEncrypted()) { PDDocumentInformation info document.getDocumentInformation(); // 进一步读取权限需要结合 AccessPermission }不过这是进阶话题第一阶段的测试不一定要覆盖到那么细。5.4 多页 PDF 的测试策略如果被测 PDF 有几十页我们通常不会对整个文档的文本做一次断言而是按页验证。PDFTextStripper支持设置起始页和结束页PDFTextStripper stripper new PDFTextStripper(); stripper.setStartPage(3); stripper.setEndPage(3); String page3Text stripper.getText(document);这种方式适合验证“每一页的页脚是否都正常”或者“第 5 页是否包含免责声明”。结合 for 循环可以批量检查每一页是否包含页号try (PDDocument document PDDocument.load(pdfFile)) { PDFTextStripper stripper new PDFTextStripper(); for (int i 1; i document.getNumberOfPages(); i) { stripper.setStartPage(i); stripper.setEndPage(i); String pageText stripper.getText(document); assertTrue(pageText.contains(Page i), 第 i 页应包含页码); } }6. 运行结果与验证把测试接入 Maven 生命周期执行测试时在项目根目录运行mvn test如果只想运行 PDF 相关的测试类mvn test -DtestPdfTextAssertionTest预期输出中会出现[INFO] Running com.example.pdf.PdfTextAssertionTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS看到BUILD SUCCESS以及Tests run的数字符合预期就说明测试已经正常生效。如果你在 IDE 中右键运行测试类正常但在命令行执行mvn test时提示no tests found for given includes这属于 JUnit 5 和 Maven Surefire 插件版本不匹配造成的经典问题。出现这个报错可以从以下三个方面排查问题现象可能原因排查方式解决方案单个测试类运行正常mvn test显示no tests found for given includesJUnit 5 依赖缺失或 Surefire 插件版本过旧检查pom.xml是否引入junit-jupiter依赖检查 Surefire 版本使用 2.22.0 以上版本 Surefire并引入junit-jupiter-engineIDE 中运行正常命令行运行报错测试类命名不符合 Surefire 默认扫描规则查看测试类名是否以Test结尾将测试类改为XxxTest命名中文编码问题伴随测试报错测试资源文件编码不一致检查project.build.sourceEncoding统一设置为 UTF-8上面第三条是经常被忽略的。如果 PDF 中有中文内容而测试代码里直接写了中文字符串那么 Maven 编译阶段的编码必须和使用 PDFBox 抽取时的编码一致否则会出现中文匹配不上的问题。建议在pom.xml中显式设置properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding maven.compiler.encodingUTF-8/maven.compiler.encoding /properties7. 常见问题与排查方法PDF 测试的坑往往不在测试框架而在 PDF 文件本身的复杂性。下面是实际项目中容易遇到的四类问题。7.1 中文内容提取不出来这是 PDF 测试中最常见的问题。如果 PDF 里使用了非嵌入的中文字体PDFBox 的PDFTextStripper可能提取出乱码甚至提取出空白。这并不是测试代码的问题而是 PDF 生成端没有正确处理字体。合理的排查顺序是先用 PDF 阅读器打开文件确认文字在阅读器里能否正常显示再用pdftotext命令行工具或者 PDFBox 的PDFTextStripper分别提取文本如果阅读器显示正常但提取为空基本可以判定是字体嵌入问题。在实际项目中建议要求 PDF 生成端在生成文件时嵌入所有用到的字体尤其对于简体中文、繁体中文、特殊符号。嵌入字体的 PDF 不仅测试稳定跨平台查看也不容易出现显示差异。7.2 文本顺序与预期不一致PDFBox 提取文本时严格按照内容流中的绘制顺序输出而不是按视觉上的阅读顺序。如果 PDF 的生成顺序和视觉顺序不一致比如先画了右上角的文本框再画左上角的标题抽取出的文本顺序就会让断言措手不及。对于这种情况有两个思路如果业务允许使用contains断言而不是顺序断言。如果需要验证多个字段同时存在用集合断言比如提取出内容后 split 成集合再assertTrue判断集合中同时包含多个关键词。7.3 加密 PDF 无法加载调用PDDocument.load加载加密 PDF 时如果已经拿到用户密码需要这样写PDDocument document PDDocument.load(pdfFile, userPassword);如果只有所有者密码需要加一个参数PDDocument document PDDocument.load(pdfFile, ownerPassword, userPassword);如果你的业务系统会生成加密 PDF测试用例中建议明确区分两类测试一类是校验正常内容通过传入正确的密码断言内容正确另一类是校验加密权限不输入密码直接加载断言应抛出InvalidPasswordException。这样才覆盖得完整。7.4 PDF 文件太大导致测试缓慢如果业务 PDF 有几百页每跑一次测试都重新加载并提取全文测试开销会很大。建议针对这种情况做两个优化使用PDFTextStripper的setStartPage和setEndPage定位到需要验证的页面而不是每次都提取全文。如果 PDF 很大可以考虑把测试文件分割或抽样不要在单测中加载超大文件。7.5 表格结构验证失败PDFBox 的文本提取不具备表格结构还原能力。如果你需要验证表格列数据是否正确而提取出的文本已经无法区分表格单元格边界建议改用其他手段比如将 PDF 渲染成图片后做区域 OCR或者直接用视觉回归工具对比表格区域。这个方案成本更高一般只有在内容层验证完全无法覆盖时才使用。8. 最佳实践与工程建议PDF 测试看起来简单但要在真实项目中稳定落地还需要一些工程层面的设计。8.1 测试夹具与样例文件管理PDF 测试文件属于二进制资源建议统一放在src/test/resources/pdf-sample/目录下并通过一个工具类提供访问入口。每个样例文件都要在命名中体现业务场景比如contract-v2-signed.pdf、invoice-encrypted.pdf。动态生成的样张文件建议在测试结束后立刻清理避免污染本地工作区。8.2 断言层次从粗到细写断言时建议先做粗粒度验证再做细粒度验证。最外层的页面数量断言成本最低中间层的元数据断言可以快速排查模板配置问题最后才是文本内容断言。如果一个 PDF 页面数量都不对后续文本断言大概率也会失败这时先暴露页面数量错误对排查问题更友好。8.3 把 PDF 测试纳入 CIPDF 测试适合放到 Maven 的test阶段这样每次mvn package都会自动执行。如果项目中存在体积较大的 PDF 测试资源也可以单独建一个pdf-testprofile让开发者在本地可以跳过CI 中再额外跑一遍profiles profile idpdf-test/id build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration includes include**/*PdfTest.java/include include**/Pdf*Test.java/include /includes /configuration /plugin /plugins /build /profile /profiles启动时使用mvn test -Ppdf-test这样既保证了本地常规构建的速度又能在 CI 中稳定执行 PDF 回归测试。8.4 注意测试生成文件与真实线上文件的差异很多团队会用统一的测试模板生成 PDF 来做断言但发布到线上后真实 PDF 可能是由另一个服务拼接、盖章、加签生成的。建议测试中至少覆盖两条路径一条是“生成一张全新 PDF”验证模板和字段是否正常另一条是“打开一个历史线上文件”验证兼容性和旧数据展示是否正常。后一种测试能发现很多模板版本升级引发的问题。8.5 安全边界与敏感信息处理如果被测 PDF 中包含客户手机号、身份证号、银行账号等敏感信息测试代码不要把这些信息硬编码到 GitHub 仓库和 CSDN 教程中。更合理的做法是使用脱敏后的测试数据或者在 CI 中通过环境变量动态注入。涉及加密 PDF 时密码尽量放在测试配置的application-test.yml或 CI 的 secret 中不要直接写到 Java 源码里。9. 总结与后续深入方向本文围绕“Tests for a PDF”这个主题从 PDF 测试的难点出发介绍了使用 Apache PDFBox 在 Java 项目中做 PDF 自动化测试的完整路径环境准备、文本提取断言、元数据断言、页面属性断言、Maven 集成和常见问题排查。你会发现在 Maven 构建中出现no tests found for given includes这样的报错大多数情况下和 JUnit 5 与 Surefire 版本有关排查思路已经从测试类命名、依赖版本、插件配置三个方向给出了清单。PDF 测试不一定要做到像素级对比也不一定需要重金购买商业工具。先把内容层和结构层的断言写好就已经能覆盖绝大多数业务回归场景。当你跑通这批测试并接入 CI 之后再遇到模板调整、字段删改就会有一种“心里有底”的感觉。下一步可以继续深入的方向有三个把 PDFBox 的坐标提取和视觉渲染结合做更精细的版面断言。对生成 PDF 的模板做版本差异对比引入基线文件管理。在实践中逐步沉淀一套公司内部使用的 PDF 测试断言工具类覆盖中英文混合、签名区检测、二维码可读性等特殊场景。如果这篇文章对你有帮助建议收藏备用。遇到 PDF 测试相关的新问题欢迎在评论区留言讨论。
返回列表