
直接说结论Execution failed for task :xxxx-api:test. No tests found for given includes:这行报错八成不是你的测试代码写挂了而是 Gradle 在 test 任务阶段压根没发现任何它认为需要测试的类。这个报错在 Java/Kotlin 多模块项目里尤其常见特别是从命令行敲./gradlew test或者 CI 构建跑到 test 阶段时冷不丁就给你来这么一下。我第一次在一套基于 Spring Boot 的多模块工程里遇到它时第一反应是去查测试类是不是被误删了查了半天发现测试类都在最后才知道问题根本不在测试代码本身。这篇博客不整虚的我想把从报错出现、定位、修复再到未来怎么避免的完整思路写一遍适合正在被 Gradle 测试任务搞得头疼的同学也适合想彻底搞懂 test 任务行为的人。1. 报错第一现场先弄懂 Gradle 在抱怨什么1.1 日志拆解一句话其实包含了三个信息Execution failed for task :xxxx-api:test. No tests found for given includes:并不是一条笼统的报错把它拆开看三块信息都很明确Execution failed for task :xxxx-api:test失败的是xxxx-api这个模块下的test任务不是整个构建链也不是其他模块。如果你的项目有几十个模块这一句直接帮你把排查范围缩小到单个模块。 No tests found for given includes:这是失败原因。意思是 test 任务在执行时带着一组 include 过滤条件最后匹配到的测试类数量是 0。冒号后面还有一段可选信息。有时候会跟着具体的过滤条件比如[com.xxx.xxxTest]有时候什么都没有。如果括号内为空说明根本没有显式 include那问题大概率出在默认的测试发现规则上。given includes这个词值得多说一句test 任务维护着一组我打算跑哪些测试类的规则调用方可以通过命令行参数--tests、构建脚本里的filter块、甚至某些插件默认配置来动态调整它。每一条规则到这里都会变成一个 include 条件。报错时冒号后面列出来的就是这些条件的当前值。1.2 这个失败和测试真的跑挂了是两码事我最开始被这个报错误导是因为它出现的形态和测试失败太像了——都是在 Gradle 控制台里刷红色都会让 CI 的构建状态变红。但两者发生的阶段完全不同。测试用例失败时JUnit 引擎已经加载了测试类、执行了用例、抛出了断言异常test 任务在执行阶段结束时标记为失败而且会留下完整的测试报告里面能精确看到哪个用例、哪一行断言挂掉。而No tests found是发生在执行用例之前Gradle 先编译测试代码然后在编译产物里做类扫描按照 include 条件去匹配测试类。如果匹配结果是 0构建直接失败测试根本没有跑起来。Gradle 之所以这么设计是为了避免出现测试任务跑完一个测试都没跑但 CI 还是绿的这种假平安。旧版本里没有匹配到测试只是跳过任务很多人没意识到 CI 其实已经变成摆设了后来 Gradle 把这种行为改成显式失败倒逼开发者去正视问题。理解了这一点什么情况下会报这个错就比较容易推导了。2. 四大高频诱因先别改代码按清单排查2.1 类名不符合 Gradle 默认测试发现规则这是所有诱因里出现概率最高的一个。Gradle 的 test 任务天生自带一套默认的类名过滤规则只有满足特定后缀的类才会被当成测试类**/*Test.class**/*Tests.class**/*TestCase.class如果你用的是 Groovy 这类 JVM 语言写测试还会额外匹配**/*Spec.class这类命名风格。关键点来了如果一个测试类叫UserServiceCheck.java、UserServiceValidator.java它在 IDE 里可以被正常右键运行但当你直接在命令行敲./gradlew :xxxx-api:test时Gradle 扫描 test classpath发现没有任何类满足默认 include 模式于是直接报No tests found for given includes:。为什么 IDE 能跑而命令行不能跑因为 IDE 在 Gradle 项目里运行测试时实际生成的命令会显式带上--tests UserServiceCheck把类名作为显式 include 条件传进去这绕过了默认命名规则。很多人被这个现象带偏以为是 Gradle 和 IDE 的兼容问题实际上是 IDE 帮你隐式修正了过滤条件。解决方式有两种。推荐第一种把测试类重命名为UserServiceTest.java遵循社区默认规则这样所有环节的测试发现机制都能正常工作不用额外配置。如果因为某些历史原因不能改类名可以在模块的 test 任务里显式补充 include 规则具体配置我放到后面的实操部分讲。2.2 build.gradle 里全局 filter 画了个太小的圈第二种高频原因是某个构建脚本或公共插件给 test 任务加了显式的测试过滤条件而这个条件与实际测试类的包路径或命名对不上。一个典型场景是这样的某个公共 gradle 文件里写了类似下面的配置tasks.withType(Test).configureEach { filter { includeTestsMatching com.demo.api.* } }这段配置的意思是所有模块的 test 任务只运行com.demo.api包下的测试类。如果你的测试类实际分布在com.demo.common、com.demo.service等包下那么匹配结果就是 0报错自然出现。最坑的地方在于这种配置经常不是写在某个模块自己的 build.gradle 里而是来自项目根目录的构建脚本、依赖的插件甚至公司内部的公共 gradle 脚本。排查时必须全局搜索includeTestsMatching、excludeTestsMatching、include、exclude这些关键字找到所有可能在给 test 任务加过滤条件的地方。如果你设置了 includeTestsMatching 又同时设置了 excludeTestsMatching两者叠加也可能导致误杀。比如 include 的是全包但 exclude 里点名排除了一些类而这些类恰恰是项目里唯一符合测试类特征的那几个。这种配置交叉很容易出现逻辑上想当然实际上匹配数为 0的结果。2.3 JUnit 版本与 Gradle 配置不匹配引擎根本没起来这个原因隐蔽性更强特别是在老项目从 JUnit 4 升级到 JUnit 5或者反过来在新建项目里混用了两套注解时容易出现。JUnit 5 的底层架构和 JUnit 4 完全不同。JUnit 4 默认通过基于反射的扫描机制识测试类而 JUnit 5 引入了平台Platform概念需要独立的测试引擎来发现和执行测试。也就是说Gradle 必须调用useJUnitPlatform()然后由对应的引擎去扫描测试类。如果项目已经引入了 JUnit 5 的注解但 build.gradle 里没有开启平台支持测试类在 Gradle 眼里就是一堆普通类No tests found就是这么来的。还有一种更细微的依赖问题。很多项目的 dependencies 里只有testImplementation org.junit.jupiter:junit-jupiter-api:5.10.2却遗漏了真正负责执行测试的引擎依赖junit-jupiter-engine。API 只是注解和接口的集合引擎才是干活的那个人。没有引擎测试发现机制会直接宣告失败报的错就是找不到测试。混用 JUnit 4 和 5 的项目更要小心。如果旧测试类用的是org.junit.Test注解而 Gradle 配置打开的是 JUnit Platform那么 JUnit 4 的测试类想被发现必须额外引入 vintage 引擎。缺少 vintage 引擎时JUnit 5 的类能跑JUnit 4 的老类全部消失报错信息依然指向找不到测试。2.4 测试源码目录和 sourceSet 配置错位这一条属于比较基础的坑但新人和重构期项目都容易踩。测试代码必须放在对应的源码集里才能被 test 任务编译和扫描这是写 Java 项目的基本约定但实际操作中总会出现意外。最典型的是测试类写到了src/main/java下面。这种情况常见于一些教程或项目迁移时的临时操作类被编进了 main 产物而 test 阶段扫描的是 test classpath自然什么都找不到。另一种情况是构建脚本里手动改过 sourceSets比如sourceSets { test { java { srcDirs [src/test] } } }而实际文件目录还是默认的src/test/java。目录错位之后test 任务的输入里一份 java 文件都没有同样会触发找不到测试类。注意这里有个容易混淆的点如果模块完全没有测试源码Gradle 通常会报NO-SOURCE并跳过任务而不是报No tests found。但一旦有全局 filter 的存在即使没有测试源码也会先执行 filter 匹配匹配不到就直接报No tests found。这种组合情况排查起来更绕人。3. 实操排查全流程从复现到修复的五个步骤3.1 第一步先确认 test 任务的真实形态排查一开始不要急着改配置先把目标任务的现状看清楚。在模块目录下执行./gradlew :xxxx-api:tasks --all | grep -i test这条命令会列出所有名称里带 test 的任务包括标准 test、testClasses、compileTestJava 等。确认你要跑的就是test还要留意是否有构建脚本或插件替换了默认任务比如把 test 换成了 integrationTest或者给 test 加了onlyIf条件。很多项目的找不到测试其实是任务跑错了或者任务被改成只跑某个特殊分组。再顺手看下任务是否会被真正执行./gradlew :xxxx-api:test --dry-run如果输出里带有SKIPPED标记先找原因有可能是onlyIf条件不满足有可能是任务在任务图中根本不在当前执行链上。这一步能把很多执行环境差异排除掉。3.2 第二步用 --info 日志定位过滤条件正常的 Gradle 配置不要轻易开--debug日志量太大反而干扰判断。对这次排查来说--info已经足够./gradlew :xxxx-api:test --info运行后重点搜索以下关键词No tests found确认报错位置。Filtering tests based on显示当前生效的过滤条件来自哪里这是最有价值的一行。Skipping task :xxxx-api:test as it has no source files如果看到这一行说明 test 任务压根没拿到测试源码。如果日志里出现了完整的过滤条件直接和测试类的实际包名、类名对比基本能定位出是哪一层配置把测试类筛掉的。如果日志里的过滤条件为空说明问题出在默认的类名扫描规则上直接跳去检查测试类命名。3.3 第三步核对源码、编译产物和构建配置日志给的是线索下面要把线索落到文件上。先确认测试源码是否存在find xxxx-api/src/test -name *.java | sort如果结果为空问题很直接测试源码不在预期目录里。如果结果不为空接着看编译产物是否生成find xxxx-api/build/classes/java/test -name *Test*.class | sort产物目录为空或目录不存在说明测试代码没有进入编译流程。这时先单独执行编译任务./gradlew :xxxx-api:compileTestJava --info如果编译阶段报错比如测试类里引用了其他模块的测试工具类但拿不到依赖这就会导致 test 任务阶段没有 class 文件可用报错表面上是找不到测试实际原因是编译失败。如果编译成功但产物目录依旧为空那基本可以断定源码没放在 test sourceSet 的扫描路径下回去检查 sourceSets 配置。3.4 第四步清理环境后二次复现某些情况下本地项目因为增量缓存、守护进程状态异常会表现出和干净环境完全不同的行为。我的建议是按顺序做这几件事./gradlew --stop ./gradlew :xxxx-api:clean--stop是停掉所有 Gradle 守护进程避免长时间运行的 daemon 带着旧的项目状态参与新任务。clean清理的是 build 目录这个操作会顺带清掉旧的编译产物和测试报告。做完后再跑一次./gradlew :xxxx-api:test --info如果清完环境后报错消失说明问题是增量状态或守护进程缓存污染这类问题虽然不算高频但遇到一次就能让人卡半天。需要注意不要去删整个.gradle缓存目录那会影响所有项目风险比较大只有确认是全局缓存问题且项目可重建时才考虑。3.5 第五步按诱因对症下药把前面的结论整理成一张速查表实际操作时可以对照处理现象可能诱因修复建议命令行默认跑挂IDE 显式指定类能跑测试类命名不符合默认规则重命名为*Test、*Tests、*TestCase项目所有模块的 test 全部挂公共脚本或插件里的 filter 匹配不到任何类全局搜索 include/exclude 配置并修正测试类有 Test 注解但日志显示扫描不到JUnit 5 未开启 useJUnitPlatform或缺少引擎依赖开启平台支持补齐junit-jupiter-engine已有测试类但编译产物目录为空测试代码放错了 sourceSet 或 sourceSets 配置错位调整源码目录或修正 sourceSets 配置4. 我踩过的真实坑四个典型场景全过程还原4.1 场景一IDE 里跑得好好的命令行一跑就挂某次在一个电商业务模块里我写完测试类后直接在 IDE 里右键跑全绿很放心地提交。结果 CI 挂在了:xxxx-api:test这一步报的正是No tests found for given includes:。我按排查流程走到第二步时发现日志里的过滤条件指向了一个公共构建脚本。那是项目里一个公共 gradle 文件里面用tasks.withType(Test)给所有模块设置了includeTestsMatching *Check。也就是说所有模块的 test 任务都被限定为只跑以Check结尾的测试类。而我新建的测试类用的后缀是Test自然全部匹配不到。修复并不复杂在xxxx-api模块的 test 配置里覆盖掉这个继承来的 filtertest { filter { includeTestsMatching *Test includeTestsMatching *Tests } }那次之后我养成了一个习惯任何 Gradle 项目拿到手第一件事就是在根目录全局搜索includeTestsMatching和excludeTestsMatching看有没有公共测试过滤规则。因为这类配置写在哪里都不奇怪报错模块本身的 build.gradle 往往反而是干净的。4.2 场景二多模块依赖里测试类凭空消失另一个模块当时表现得更诡异测试源码清清楚楚写在src/test/java里类名也符合规范但 test 任务就是提示找不到测试。我甚至一度怀疑是不是 Gradle 缓存坏了直到我看了一眼build/classes/java/test里面空空如也。去跑compileTestJava报出来的错误是api 模块的测试类里 import 了公共模块的测试工具类而公共模块的测试工具类并没有被打包或发布。我当时用的依赖写法是testImplementation project(:common)这个写法只能访问 common 模块的 main 源码集产物也就是生产代码common 模块src/test下的工具类根本不会被当作依赖输出。这类需求的正确解法是用java-test-fixtures插件。在公共模块里启用插件把要共享的测试工具类放到src/testFixtures/java下然后在 api 模块里写testImplementation testFixtures(project(:common))这样测试工具类才能作为独立输出被下游模块的测试代码使用。这个坑的教训是报错写着没有测试不代表问题一定出在测试发现机制上还有可能是测试类根本没编译出来。编译失败信息可能被后续的 test 任务失败信息遮住排查时一定要先确认build/classes/java/test目录里有东西。4.3 场景三参数化测试方法级过滤不生效还有一次是调试单个方法时遇到的。在 IDE 里看到参数化测试方法的显示名带着[1]、[2]这种参数编号后缀于是我在命令行里想通过--tests com.demo.MyServiceTest.paramTest只跑这个用例结果报No tests found for given includes。这是因为参数化测试在 JUnit 5 平台上的注册标识和源码方法名并不是完全一致的。Gradle 在解析--tests参数时面对参数化测试的动态实例方法级过滤匹配往往对不上号。实践中最稳妥的处理方式是先按类过滤跑通整个类的所有方法再确认是否真的需要只跑其中某一个参数实例。如果需要精确定位到某个参数得根据 IDE 生成的命令行来对照写法而不是凭源码方法名盲猜。另外方法名里如果有特殊字符或中文也存在终端编码不一致导致命令行解析失败的风险。我后来遇到方法级过滤报错时都会先做一次类能跑、方法不能跑的对照实验确认问题出在过滤语法还是测试本身。4.4 场景四本地全绿CI 上却挂掉这类问题最磨人因为本地复现不出来。一次 CI 构建里api 模块报No tests found而我本地用同样的命令跑是好的。比对了一圈环境差异最后定位到 JDK 版本不同。CI 上默认用的 JDK 版本和本地不一测试类里的某些写法在旧 JDK 环境下解析失败Gradle 的测试发现机制扫描时直接抛错或跳过这一类。解决方式是统一项目构建环境最好的做法是引入 Gradle Toolchain 配置让项目明确定义所需的 JDK 版本java { toolchain { languageVersion JavaLanguageVersion.of(17) } }同时在 CI 侧固定基础镜像的 JDK 版本。CI 上还要注意别把构建搞成不干净复现有些流水线配置是直接跑./gradlew build如果构建目录带着之前的历史产物测试类扫描可能基于过期的 class 文件进行。遇到本地与 CI 不一致时先用./gradlew clean test排除增量缓存干扰再检查 JDK 和依赖缓存。5. 让 test 任务从此稳定配置建议与日常习惯5.1 统一测试命名规范不给默认规则添乱项目里最值得定死的一件事就是测试类的命名规范。*Test、*Tests、*TestCase这三种后缀是 Gradle 默认能识别的也是社区里接受度最高的。团队约定俗成使用*Test作为统一后缀其他命名一律不允许出现在 test 源码集里。另外一个细节是测试类要保持public。JUnit 扫描测试类的可见性要求会在某些版本下变得严格非 public 的测试内层类或方法可能直接跳过。这不是报错信息最直接的诱因但在为什么我这边能跑你那边就跑不了的追溯中我见过好几次因为测试类可见性导致的假性消失。分组和筛选也尽量用Tag注解而不是在 build.gradle 里写死 include/exclude。比如把慢测试标记为Tag(slow)再用--tests配合--tags做运行时过滤这样源码里保留了测试的完整集合只是执行时动态筛选要比修改 include 条件安全得多。5.2 一套可复制的 test 配置模板我比较推荐的基础配置是这样的test { useJUnitPlatform() testLogging { events passed, skipped, failed showStandardStreams false exceptionFormat full } systemProperty(file.encoding, UTF-8) }useJUnitPlatform()是 JUnit 5 项目的标配现在新项目基本都是它。testLogging里的exceptionFormat full强烈建议加上因为它能让异常的具体信息和堆栈在控制台完整输出否则 CI 日志里经常只显示一行摘要排查问题要多开一个报告页面。systemProperty(file.encoding, UTF-8)是为了防止测试里处理中文或特殊字符时出现编码不一致。另外不建议在 test 任务里搞这种配置test { onlyIf { !project.hasProperty(skipTest) } }一次两次用起来方便但一旦 CI 不小心传了skipTest参数整个测试阶段直接跳过比报错更像假运行比找不到测试的问题还要隐蔽。测试应该保持要么老老实实跑要么明确失败的确定性。5.3 CI 上跑测试前的检查清单CI 是整个测试体系的最终守护者也是找不到测试报错的高发地。我的建议是每次调整测试相关配置后按这个清单过一遍确认 CI 构建统一执行clean test避免增量产物干扰测试类扫描结果。固定 JDK 版本本地和 CI 保持完全一致优先用 Gradle Toolchain 声明项目所需版本。在 CI 日志配置中打开 testLogging 完整输出确保失败时能看到具体是哪个模块、哪个任务报错。检查构建脚本里是否有全局 filter 配置尤其是从公共 gradle 文件或插件继承下来的过滤条件。很多 CI 报错其实是这类隐藏配置导致的。测试报告产物要归档方便出问题时直接打开 HTML 报告确认到底有没有测试被执行。6. 我最想提醒你的一个排查习惯文章最后回到我自己的实操体会。现在我再遇到这个报错第一反应不是翻代码而是先跑一条./gradlew :xxxx-api:test --info直接看日志里输出的过滤条件。然后再花一分钟检查两件事测试类的后缀是不是符合默认规则编译产物目录里有没有实际生成的 class 文件。这三个动作做完八成以上找不到测试的问题都能当场定位。还有一个不为人注意的小技巧如果报错模块里查不到任何过滤配置不要停下往项目更上层的构建脚本里找。多模块工程根目录的 build.gradle、gradle/ 目录下的公共脚本、甚至插件市场自动生成的配置都可能在你看不到的层级往 test 任务里注入过滤规则。这个问题我踩过一次之后每次遇到都先全局搜一遍includeTestsMatching和excludeTestsMatching省掉了很多无意义的代码翻查。遇到这类 Gradle 疑难杂症心态放平大多数情况不是代码的问题而是构建系统跟你开了个玩笑。