在最近一个测试工程里,我遇到了一个非常“奇怪”的现象:代码明明写了@Test方法,项目也能正常编译,可一旦执行mvn test,终端却直接抛出no tests found for given includes: ...。更巧的是,这个项目的src/test/resources里放了一批 PDF 文件,用来做文档解析与内容提取测试。排查到最后才发现,问题出在测试资源和测试类的“身份混淆”上。这篇文章就把这次 PDF 测试场景的完整踩坑过程、根因分析和自动化测试方案整理出来,希望对你处理 PDF 相关测试时有所帮助。
1. 问题背景:一条诡异的测试报错
先还原一下当时的报错场景。在项目中执行测试时,Maven 显示构建成功,但测试总数为 0:
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ pdf-tests-demo --- [INFO] No tests to run.换一种运行方式,比如在 IDEA 中右键某个测试类直接运行,又会看到类似这样的报错:
no tests found for given includes: [com.example.demo.PdfContentTest]这行报错信息字面意思是“在给定的 includes 规则中,没有找到任何测试”,但它并不是说PdfContentTest这个类不存在,而是说 Maven Surefire 或 JUnit Platform 在扫描类时,没有把这个类识别为一个可执行的测试类。
这个报错和 PDF 有什么关系?主要有两种常见场景:
- 测试类命名不符合 Maven Surefire 的默认扫描规则,例如类名是
PdfContentCheck而不是PdfContentTest,导致测试根本没有进入扫描范围。 - PDF 文件被错误地放进了测试源码目录(如
src/test/java),文件格式识别异常,引发一系列预料之外的资源加载或扫描问题。
如果说你只是单纯写 PDF 文档解析功能,可能不会碰到这个报错;但一旦你把 PDF 作为测试数据、测试样本或断言对象集成到自动化测试体系中,就很可能会踩到这些“测试框架本身”的坑。本文会从根因开始讲,再带你搭建一套完整、可复用的 PDF 自动化测试项目。
2. 环境准备与版本说明
因为本文涉及具体代码,先明确一下实验环境。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
| 工具/组件 | 说明 |
|---|---|
| JDK | JDK 8 及以上版本均可,示例以 JDK 8 语法为主 |
| 构建工具 | Maven 3.6+ |
| 测试框架 | JUnit 5(JUnit Jupiter) |
| PDF 解析库 | Apache PDFBox 2.x |
| IDE | IntelliJ IDEA 或 Eclipse,可根据个人习惯选择 |
如果使用 Maven 构建项目,核心依赖需要保持版本匹配。尤其是 JUnit 5 和 Maven Surefire 插件之间存在版本联动,很多 “No tests found” 问题就是插件版本过低导致的。
这里我使用 PDFBox 2.x 版本,因为 2.x 的 API 资料最多、兼容性最好。PDFBox 3.x 已经发布,API 上有一些变化,比如
PDDocument.load(File)改为Loader.loadPDF(File),如果你的项目使用 3.x,需要留意差异。
后续的示例是一个标准的 Maven 工程,目录结构如下:
pdf-tests-demo ├── pom.xml ├── src │ ├── main │ │ └── java │ │ └── com │ │ └── example │ │ └── pdf │ │ └── PdfTextUtils.java │ └── test │ ├── java │ │ └── com │ │ └── example │ │ └── pdf │ │ └── PdfContentTest.java │ └── resources │ └── sample.pdf3. “no tests found for given includes” 的根因拆解
这个报错有一个很迷惑人的地方:它出现在构建工具、测试框架和 IDE 三套体系交会的位置,任何一层扫描失败,都可能抛出同样的信息。
3.1 Maven Surefire 的测试类扫描规则
Maven 执行mvn test时,默认使用maven-surefire-plugin扫描src/test/java下的测试类。它的默认包含规则是:
**/Test*.java**/*Test.java**/*Tests.java**/*TestCase.java
如果测试类命名为PdfContentCheck、PdfValidator,就不符合上述任意一条规则,Surefire 会直接跳过。这在报错上有时表现为“找不到测试”,有时表现为“Build 成功但没有任何测试被执行”。
因此,排查这类问题时要先确认类名是否满足默认规则。如果确实需要自定义命名,必须在pom.xml中显式配置:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> <configuration> <includes> <include>**/*Check.java</include> <include>**/*Tests.java</include> <include>**/*Test.java</include> </includes> </configuration> </plugin>3.2 测试资源与源码目录混淆
这是让我印象最深的一个坑。当时项目里需要把 PDF 样本作为测试资源,不知道哪位同事把sample.pdf直接放到了src/test/java/com/example/pdf/目录下。从 IDEA 的 Project 视图看,PDF 文件确实在那儿躺着,但 Maven 编译测试源码时,会把这个目录当作 Java 源码目录处理,虽然 PDF 不会被编译,却可能干扰 IDE 对源目录的扫描索引,导致测试类的加载出现异常。
同理,如果测试资源放错位置,运行时getResource()也可能拿不到文件。正确做法是:
- PDF 测试样本统一放到
src/test/resources/ - 测试类统一放到
src/test/java/ - 不要将非 Java 文件放到测试源码目录中
3.3 JUnit 5 与 Surefire 版本不匹配
另一个高频原因是 JUnit 5 依赖没有正确传递给 Surefire。Surefire 从 2.22.0 开始才原生支持 JUnit Platform,如果你还在用 2.12.4 之类的旧版本,或者项目并没有显式引入junit-jupiter-engine,就会出现测试类虽然存在但 JUnit 平台找不到入口的情况。
下面的pom.xml依赖组合是 JUnit 5 官方推荐的基础配置:
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> </dependencies>3.4 反模式案例:PDF 文件导致的测试静默失败
有一个容易忽略的细节是:测试类中如果通过@BeforeAll加载 PDF 资源,而 PDF 路径写错或文件损坏,异常可能被吞掉,测试类初始化失败,JUnit 认为没有任何可执行的测试方法,最终也会显示no tests found。
例如这样不规范的写法:
@BeforeAll static void init() throws IOException { // 如果路径写错,整个测试类初始化失败 File pdfFile = new File("src/test/resources/missing.pdf"); PDDocument.load(pdfFile); }一旦missing.pdf不存在,PDDocument.load会抛出异常,测试类初始化失败,JUnit 平台把整个类标记为不可执行。因此,加载测试资源时应优先使用类路径方式,增加可移植性。
4. PDF 自动化测试完整实战
排错之后,我们进入正题:如何把 PDF 文件纳入自动化测试体系。下面通过一个完整的 Maven 工程,演示 PDF 文本抽取、内容断言、页数校验和元数据校验的方法。
4.1 添加 Maven 依赖
创建项目后,先在pom.xml中添加 JUnit 5 与 PDFBox 依赖:
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.27</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>4.2 准备测试用 PDF
为了不依赖外部网络资源,我们可以用代码生成一个最简单的 PDF 文件,写入固定的测试文本。这样每次测试都能复现,不会出现“外部 PDF 下载失败”的问题。
生成 PDF 的代码可以单独写成一个工具方法,放在src/test/java/com/example/pdf/PdfTestDataGenerator.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.font.PDType1Font; import java.io.IOException; /** * 测试数据生成器:用于生成包含指定文本的 PDF 文件。 */ public class PdfTestDataGenerator { private PdfTestDataGenerator() { } public static void createSamplePdf(String outputPath, String content) throws IOException { try (PDDocument document = new PDDocument()) { PDPage page = new PDPage(); document.addPage(page); try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { contentStream.beginText(); contentStream.setFont(PDType1Font.HELVETICA, 14); contentStream.newLineAtOffset(100, 700); contentStream.showText(content); contentStream.endText(); } document.save(outputPath); } } }这里用到了 PDFBox 的PDDocument、PDPage、PDPageContentStream三个核心类,分别负责文档对象、页面对象和内容流写入。代码中的try-with-resources语法确保文档流被正确关闭,避免内存和文件句柄泄漏。
4.3 编写 PDF 文本抽取工具类
在main代码目录下,我们创建一个文本抽取工具类,后续测试全部复用这个工具:
package com.example.pdf; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import java.io.IOException; import java.io.InputStream; /** * PDF 文本抽取工具。 */ public class PdfTextUtils { private PdfTextUtils() { } public static String extractText(InputStream inputStream) throws IOException { try (PDDocument document = PDDocument.load(inputStream)) { PDFTextStripper stripper = new PDFTextStripper(); return stripper.getText(document); } } public static int countPages(InputStream inputStream) throws IOException { try (PDDocument document = PDDocument.load(inputStream)) { return document.getNumberOfPages(); } } }在 PDFBox 2.x 中,PDDocument.load(InputStream)会读取整个 PDF 文件到内存中,因此解析完以后必须关闭。PDFTextStripper是 PDFBox 提供的文本抽取器,它会按页面顺序提取可见文本内容。
4.4 编写 PDF 内容断言测试
下面编写核心测试类,注意命名必须符合 Maven Surefire 的默认规则:
package com.example.pdf; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.io.InputStream; import static org.junit.jupiter.api.Assertions.*; /** * PDF 内容抽取测试。 */ class PdfContentTest { private static InputStream pdfInputStream; @BeforeAll static void setup() throws Exception { // 从 classpath 中加载测试资源,避免文件路径在不同环境下不一致 pdfInputStream = PdfContentTest.class.getResourceAsStream("/sample.pdf"); assertNotNull(pdfInputStream, "sample.pdf 未找到,请确认文件位于 src/test/resources 目录"); } @Test @DisplayName("PDF 应包含指定文本") void shouldContainExpectedText() throws Exception { String content = PdfTextUtils.extractText(pdfInputStream); assertTrue(content.contains("Hello PDF Test"), "PDF 中未找到目标文本"); } @Test @DisplayName("PDF 页数应为 1") void shouldHaveOnePage() throws Exception { int pages = PdfTextUtils.countPages(pdfInputStream); assertEquals(1, pages, "PDF 页数不符合预期"); } @Test @DisplayName("PDF 不应包含非法敏感词") void shouldNotContainForbiddenText() throws Exception { String content = PdfTextUtils.extractText(pdfInputStream); assertFalse(content.contains("Internal Error"), "PDF 中包含不应出现的文本"); } }这里有一个值得注意的细节:@BeforeAll方法是静态的,它在每个测试方法执行前只运行一次。我们把 PDF 输入流放在setup()中统一加载,避免每个测试方法重复读取文件。但由于同一个InputStream在第一次解析后可能已经读完,多个测试方法共用一个流时,要么在每次抽取前重新打开,要么在工具类内部自己加载流。
上面这个例子如果直接跑,第二个和第三个测试方法有可能读到空内容。更稳妥的做法是每个测试方法独立打开流:
@Test @DisplayName("PDF 应包含指定文本") void shouldContainExpectedText() throws Exception { try (InputStream is = PdfContentTest.class.getResourceAsStream("/sample.pdf")) { String content = PdfTextUtils.extractText(is); assertTrue(content.contains("Hello PDF Test"), "PDF 中未找到目标文本"); } }关于这一点,强烈建议理解“流是一次性资源”这个概念。很多 PDF 相关测试的“随机失败”,最后都归结为流复用问题。
4.5 生成测试 PDF 并运行
首先用生成器创建测试文件。可以在main方法中直接调用,也可以写一个独立的测试类来生成。为了不污染正式测试结果,建议生成逻辑放在test代码目录,并且只在需要的时候手动执行。
package com.example.pdf; import org.junit.jupiter.api.Test; import java.nio.file.Paths; class PdfTestDataGeneratorTest { @Test void generateSamplePdf() throws Exception { String outputPath = Paths.get("src/test/resources/sample.pdf").toAbsolutePath().toString(); PdfTestDataGenerator.createSamplePdf(outputPath, "Hello PDF Test"); System.out.println("PDF 已生成:" + outputPath); } }执行该测试类后,src/test/resources/sample.pdf会生成一个包含Hello PDF Test文本的单页 PDF。接下来在项目根目录运行:
mvn test预期输出中会看到类似下面的结果:
[INFO] Running com.example.pdf.PdfContentTest [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0如果你还能看到Tests run: 3,说明测试被成功识别并执行了,“no tests found for given includes” 的问题已经解决。
4.6 扩展测试场景:页数、元数据与图片
PDF 测试不止文本断言。在实际项目中,常常需要验证:
- 生成的报告是否包含正确的页数。
- PDF 的标题、作者等元数据是否正确。
- PDF 中是否包含指定图片。
- 接口返回的 PDF 是否能被正常解析。
我们可以继续扩展PdfTextUtils,添加元数据读取方法:
public static String getDocumentTitle(InputStream inputStream) throws IOException { try (PDDocument document = PDDocument.load(inputStream)) { return document.getDocumentInformation().getTitle(); } }对应测试:
@Test @DisplayName("PDF 标题应为空或指定值") void shouldHaveTitle() throws Exception { try (InputStream is = PdfContentTest.class.getResourceAsStream("/sample.pdf")) { String title = PdfTextUtils.getDocumentTitle(is); assertNull(title, "该测试 PDF 未设置标题,应为 null"); } }图片校验稍微复杂一些,需要遍历PDResources中的 XObject。示例思路如下:
for (PDPage page : document.getPages()) { PDResources resources = page.getResources(); for (COSName name : resources.getXObjectNames()) { PDXObject xObject = resources.getXObject(name); if (xObject instanceof PDImageXObject) { // 说明页面中包含图片 } } }这段代码只是核心片段,你需要放入对应的工具类中,并引入org.apache.pdfbox.cos.COSName、org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject等类。
5. 常见问题与排查思路
下面是 PDF 测试过程中最常遇到的几个问题,建议收藏备用。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
no tests found for given includes | 测试类命名不符合 Surefire 默认规则,或 JUnit 依赖缺失 | 检查类名是否以Test结尾,检查junit-jupiter-engine依赖,升级 Surefire 到 2.22.0+ |
| 测试类能编译但测试数为 0 | PDF 文件放到了测试源码目录,IDE 索引异常 | 将 PDF 移到src/test/resources,清理 IDE 缓存后重新构建 |
| 从 classpath 加载 PDF 返回 null | 文件不在 resources 目录,或目录被排除 | 检查文件位置,确认target/test-classes下是否生成了 PDF 副本 |
解析 PDF 时提示IOException: Error loading PDF | PDF 文件损坏或不是有效 PDF | 用 PDF 阅读器打开确认文件可用,检查文件扩展名是否被伪装 |
| 抽取中文文本乱码 | PDF 使用了非标准编码,或字体映射缺失 | 确认 PDF 是否包含 Unicode 文本层;扫描版 PDF 需要 OCR 支持 |
| 多个测试方法共用 InputStream 导致数据返回空 | 流是一次性资源,读取后指针已到末尾 | 每个测试方法独立打开流,或使用@TempDir复制临时文件 |
PDFBox 3.x 中PDDocument.load(File)报错 | 3.x 的 API 改为Loader.loadPDF | 根据版本调整加载方式,升级项目时检索全部PDDocument.load调用 |
其中,@TempDir是 JUnit 5 提供的临时目录注解,特别适合需要临时生成 PDF、然后断言解析结果的场景。例如:
@Test void generateAndParseTemporaryPdf(@TempDir Path tempDir) throws Exception { Path pdfFile = tempDir.resolve("temp.pdf"); PdfTestDataGenerator.createSamplePdf(pdfFile.toString(), "Temporary Content"); try (InputStream is = Files.newInputStream(pdfFile)) { String content = PdfTextUtils.extractText(is); assertTrue(content.contains("Temporary Content")); } }使用临时目录可以避免测试用例之间相互污染,也无需手动清理文件,强烈推荐在 PDF 测试中使用。
6. 最佳实践与工程建议
6.1 测试资源统一管理
PDF 测试样本应当放在src/test/resources下,而不是散落在系统任意路径。如果你的测试需要多份 PDF,建议按照业务类型划分子目录:
src/test/resources/pdf/ ├── 单页文本.pdf ├── 多页合同.pdf ├── 含图片报告.pdf └── 空文件.pdf加载方式优先使用 ClassLoader 的getResource("/pdf/xxx.pdf"),避免使用绝对路径。这样项目换机器、换 CI 环境时,测试不会因为路径问题失败。
6.2 测试数据要小而可控
PDF 文件如果太大,会明显拖慢测试速度。通常建议:
- 单份测试 PDF 控制在几 KB 到几百 KB。
- 只在必要的场景使用真实业务 PDF,其余用代码临时生成。
- 对 PDF 解析性能有要求的场景,不要把大文件解析放在单元测试中,应该使用独立的性能测试工程。
6.3 不要直接修改原始 PDF 样本
测试中如果需要对 PDF 增加水印、拆分页面或转换格式,优先复制到临时目录再操作,避免改变测试资源的原始状态。否则多次运行测试后,样本被“污染”,后续断言全部失败。
这一点在测试 PDF 转换功能时尤其重要。例如验证“PDF 转 Word”功能,若把输出文件直接覆盖到测试资源路径,第二次运行就会读取到已经转换过的文件。
6.4 关注安全与合规
在自动化测试处理 PDF 时,需要注意权限与合规问题:
- 只解析、生成、修改你有合法权利的 PDF 文档。
- 涉及 PDF 去水印、解密等操作时,必须确保自己拥有文件的使用权限或版权授权。
- 不要在生产环境运行解析不可信来源的恶意 PDF,PDFBox 在解析恶意构造的文件时也可能存在安全风险,建议隔离运行或限制来源。
6.5 测试命名与运行策略
测试类命名是解决 “No tests found” 问题的最有效手段。建议团队统一规范:
- 单元测试统一使用
XxxTest。 - 集成测试统一使用
XxxIT,并配合 Failsafe 插件在独立阶段执行。 - 临时生成数据的测试类建议加
DataGenerator后缀,避免被误认为业务测试。
如果需要跳过某些 PDF 测试,可以使用 JUnit 5 的@Disabled注解,并写明原因:
@Test @Disabled("该测试依赖 OCR 环境,默认不执行") void ocrPdfTest() { // ... }6.6 日志与断言节奏
PDF 解析结果往往不稳定,尤其是碰到带大量图片的扫描版 PDF。建议断言时不要直接把完整文本打印到控制台,避免日志瞬间刷屏。可以先打印关键片段,再断言关键内容。其实通过断言失败信息查看输出片段,也是足够的。
7. 总结
从一次no tests found for given includes的排查开始,我重新梳理了 PDF 测试资源在 Maven 工程中的摆放规范,以及 PDFBox 在自动化测试中的常用姿势。需要牢记的还是那几个点:测试类命名要符合 Surefire 默认规则、PDF 文件不能混入测试源码目录、InputStream 不能跨测试方法复用。掌握这些之后,再用 PDFBox 做文本抽取、页数校验、元数据校验就会顺手很多。
如果你正在做文档解析类系统,或者需要在 CI 里自动校验导出的 PDF 报告,本文的项目结构和测试示例可以直接复用。下一步可以根据你的业务需求,继续扩展多页 PDF 校验、扫描版 PDF 的 OCR 识别校验,以及 PDF 转 Word、PDF 转 Excel 等转换链路的自动化断言。不要心急,把基础样本和断言工具搭好,后面扩展会非常快。