PDF测试遇“no tests found”?Maven Surefire与JUnit 5排查实战
2026/8/28 23:38:03 网站建设 项目流程

在最近一个测试工程里,我遇到了一个非常“奇怪”的现象:代码明明写了@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 有什么关系?主要有两种常见场景:

  1. 测试类命名不符合 Maven Surefire 的默认扫描规则,例如类名是PdfContentCheck而不是PdfContentTest,导致测试根本没有进入扫描范围。
  2. PDF 文件被错误地放进了测试源码目录(如src/test/java),文件格式识别异常,引发一系列预料之外的资源加载或扫描问题。

如果说你只是单纯写 PDF 文档解析功能,可能不会碰到这个报错;但一旦你把 PDF 作为测试数据、测试样本或断言对象集成到自动化测试体系中,就很可能会踩到这些“测试框架本身”的坑。本文会从根因开始讲,再带你搭建一套完整、可复用的 PDF 自动化测试项目。

2. 环境准备与版本说明

因为本文涉及具体代码,先明确一下实验环境。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。

工具/组件说明
JDKJDK 8 及以上版本均可,示例以 JDK 8 语法为主
构建工具Maven 3.6+
测试框架JUnit 5(JUnit Jupiter)
PDF 解析库Apache PDFBox 2.x
IDEIntelliJ 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.pdf

3. “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

如果测试类命名为PdfContentCheckPdfValidator,就不符合上述任意一条规则,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 的PDDocumentPDPagePDPageContentStream三个核心类,分别负责文档对象、页面对象和内容流写入。代码中的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 测试不止文本断言。在实际项目中,常常需要验证:

  1. 生成的报告是否包含正确的页数。
  2. PDF 的标题、作者等元数据是否正确。
  3. PDF 中是否包含指定图片。
  4. 接口返回的 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.COSNameorg.apache.pdfbox.pdmodel.graphics.image.PDImageXObject等类。

5. 常见问题与排查思路

下面是 PDF 测试过程中最常遇到的几个问题,建议收藏备用。

问题现象常见原因解决思路
no tests found for given includes测试类命名不符合 Surefire 默认规则,或 JUnit 依赖缺失检查类名是否以Test结尾,检查junit-jupiter-engine依赖,升级 Surefire 到 2.22.0+
测试类能编译但测试数为 0PDF 文件放到了测试源码目录,IDE 索引异常将 PDF 移到src/test/resources,清理 IDE 缓存后重新构建
从 classpath 加载 PDF 返回 null文件不在 resources 目录,或目录被排除检查文件位置,确认target/test-classes下是否生成了 PDF 副本
解析 PDF 时提示IOException: Error loading PDFPDF 文件损坏或不是有效 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 等转换链路的自动化断言。不要心急,把基础样本和断言工具搭好,后面扩展会非常快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询