主题
界面
主题
"测试写好了,怎么让它跑起来"——这个问题看起来简单,但三个具体的坑很常见:测试类没被发现、依赖没加 scope=test、集成测试被当单元测试跑。
本节把 Maven 与测试相关的约定、命令与报告位置讲清楚。这些都是"知道了就不会再踩,不知道就会反复查"的知识。
完成本节后,你应该能够:
scope=test 的作用,以及写错会有什么后果;-Dtest / -DskipTests / -Dmaven.test.skip=true 精确控制测试执行;三个真实场景:
场景一:测试写了,但 mvn test 说 "No tests to run"。
原因通常是类名不符合约定——surefire 默认只认特定命名,这不会报错,只是安静地跳过。
场景二:JUnit 依赖忘了写 scope=test,测试框架被打进了生产包。
后果是包体积变大、依赖冲突,甚至在某些容器里因为多出来的类而出错。这个错误在开发阶段完全无感,只在发布时暴露。
场景三:集成测试和单元测试混在一起跑。
每次执行都要起数据库、等网络——本该 5 秒的单元测试变成了 5 分钟,于是没人再跑它。
这三个问题的共同点是:都是"约定"没搞清楚。
testing-unit/
├── pom.xml
└── src/
├── main/
│ ├── java/ # 生产代码
│ └── resources/ # 生产资源(配置文件等)
└── test/
├── java/ # 测试代码(不进入生产包)
└── resources/ # 测试专用资源两个关键约定:
src/test/java 里的类不会被打进产物——这是 Maven 的硬保证,你不用担心测试代码泄露到生产;src/test/resources 优先于 src/main/resources 被加载(同名文件测试版本覆盖生产版本)——这是做测试配置覆盖的标准手段。依赖作用域:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope> <!-- 关键:只在测试编译与运行期可见 -->
</dependency>| scope | 编译生产代码 | 编译/运行测试 | 进入生产包 |
|---|---|---|---|
compile(默认) | ✓ | ✓ | ✓ |
test | ✗ | ✓ | ✗ |
provided | ✓ | ✓ | ✗(由运行时环境提供) |
记忆点:测试框架、断言库、Mock 库,一律 test。忘了写的后果是"生产包里多了 JUnit",得不偿失。
compile → test-compile → test → package → verify → install
(编译测试代码) (运行单元测试)几个常用命令的语义:
| 命令 | 做什么 |
|---|---|
mvn test | 编译生产代码 + 编译测试代码 + 运行单元测试 |
mvn package | 上面全部 + 打包(同时会跑测试) |
mvn verify | 上面全部 + 运行集成测试(配置了 failsafe 时) |
mvn install | 上面全部 + 装进本地仓库 |
一个容易忽略的点:mvn package 会跑测试。所以"打包时测试失败"不是环境问题,而是设计如此——这是防止把坏代码打进制品的最后一道门。
surefire(Maven 的单元测试插件)默认按类名匹配:
| 模式 | 例子 |
|---|---|
**/Test*.java | TestOrderCalculator |
**/*Test.java | OrderCalculatorTest ✓ 本项目用这种 |
**/*Tests.java | OrderCalculatorTests |
**/*TestCase.java | OrderCalculatorTestCase |
命名不符合约定 → 类不会被加载 → 安静地不执行。这就是场景一的成因。
排查方法:跑 mvn test 时注意 "Tests run" 的总数是否与预期一致。数字对不上,第一件事就是检查类名。
| 需求 | 命令 |
|---|---|
| 只跑某个类 | -Dtest=OrderCalculatorTest |
| 只跑某个方法 | -Dtest=OrderCalculatorTest#should_apply_discount_when_total_equals_threshold |
| 按通配符跑 | -Dtest=OrderCalculator* |
| 跑多个类 | -Dtest=OrderCalculatorTest,PasswordValidatorTest |
| 排除某个类 | -Dtest=!OrderCalculatorTest |
| 跳过执行(仍编译测试代码) | -DskipTests |
| 完全跳过(不编译也不运行) | -Dmaven.test.skip=true |
| 只跑某个 tag | -Dgroups=slow(配合 @Tag("slow")) |
-DskipTests 与 -Dmaven.test.skip=true 的区别很重要:
-DskipTests | -Dmaven.test.skip=true | |
|---|---|---|
| 编译测试代码 | 会 | 不会 |
| 运行测试 | 不会 | 不会 |
| 适用 | 临时跳过执行,仍希望测试代码能编译通过 | 测试代码本身有问题、或需要极速构建时 |
实践建议:优先用 -DskipTests。因为它会编译测试代码——如果测试代码有编译错误,你会立刻知道,而不是等到某次正常构建才炸。
注意:-DskipTests 不应该出现在发布流程里。它只是本地排查用的临时开关。
testing-unit/target/surefire-reports/
├── cn.kb.learn.testing.OrderCalculatorTest.txt # 每个类一个文本报告
├── cn.kb.learn.testing.OrderCalculatorTest.xml # 同名的 XML(给 CI 解析)
├── cn.kb.learn.testing.LifecycleTest.txt
├── ...文本报告的内容(实测):
-------------------------------------------------------------------------------
Test set: cn.kb.learn.testing.OrderCalculatorTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.083 s
-- in cn.kb.learn.testing.OrderCalculatorTestXML 报告的用途:CI 系统(Jenkins、GitHub Actions)解析它来渲染"测试趋势图"和"失败列表"。这就是 CI 上能看到"哪些测试失败"的原因——靠的不是控制台输出,而是这个 XML。
注意 target/ 目录不进版本库(本项目在 .gitignore 里忽略了它):报告是构建产物,每次构建重新生成。
| surefire | failsafe | |
|---|---|---|
| 目标 | 单元测试 | 集成测试 |
| 默认类名 | *Test / Test* / *Tests / *TestCase | *IT / IT* / *ITCase |
| 绑定阶段 | test | integration-test + verify |
| 失败时的行为 | 立即终止构建 | 先跑完所有测试,再在 verify 阶段报告失败 |
mvn test 会跑吗 | ✓ | ✗(要 mvn verify) |
为什么集成测试要单独一个插件:
mvn test;配置片段(在需要使用集成测试的模块里加):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.5</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>这也是本项目当前的状态:labs 只配置了 surefire(单元测试),集成测试在 T1-017 Spring 集成测试 里才引入。
@Tag("slow")
@Test
void calls_remote_service() { }./mvnw test -Dgroups=slow # 只跑慢测试
./mvnw test -DexcludedGroups=slow # 排除慢测试(本地开发常用)适用场景:把"依赖外部环境""执行时间长""需要特定数据"的测试标出来,让日常构建跑得快、让完整构建跑得全。这也是 CI 分层执行的基础(T2-017 测试流水线)。
./mvnw -pl testing-unit test -Dtest=OrderCalculatorTest执行结果(实测):
[INFO] Running cn.kb.learn.testing.OrderCalculatorTest
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.116 s
-- in cn.kb.learn.testing.OrderCalculatorTest
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS注意 Tests run: 9——只跑了这一个类的 9 个测试,其他类都没执行。
./mvnw -pl testing-unit test -Dtest=OrderCalculatorTest#should_apply_discount_when_total_equals_threshold[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.083 s
[INFO] BUILD SUCCESS这是排查单个失败用例时最有用的命令——不用等整个套件跑完。
-DskipTests 掩盖问题 ./mvnw package -DskipTests # 打包时不跑测试在紧急修复时可以用,但要注意:
mvn test。本节不新增测试代码,用 labs/testing-unit 现有工程实操。
命令一:跑整个模块的测试
cd labs
./mvnw -pl testing-unit test执行结果(实测):
[INFO] Tests run: 58, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS命令二:只跑一个测试类
./mvnw -pl testing-unit test -Dtest=OrderCalculatorTest[INFO] Running cn.kb.learn.testing.OrderCalculatorTest
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS命令三:只跑一个测试方法
./mvnw -pl testing-unit test "-Dtest=OrderCalculatorTest#should_apply_discount_when_total_equals_threshold"[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS命令四:跳过测试执行但保留编译
./mvnw -pl testing-unit test -DskipTests[INFO] Tests are skipped.
[INFO] BUILD SUCCESS命令五:查看测试报告
Get-ChildItem testing-unit/target/surefire-reportscn.kb.learn.testing.ArticleServiceTest.txt
cn.kb.learn.testing.ArticleServiceTest.xml 的同类文件...
cn.kb.learn.testing.AssertjDemoTest.txt
cn.kb.learn.testing.ExceptionAndTimeoutTest.txt
cn.kb.learn.testing.LifecycleTest.txt
cn.kb.learn.testing.LifecycleTest$PerClassLifecycle.txt
cn.kb.learn.testing.LifecycleTest$WhenQuantityIsOne.txt
cn.kb.learn.testing.OrderCalculatorTest.txt
cn.kb.learn.testing.OrderServiceTest.txt
cn.kb.learn.testing.ParameterizedOrderCalculatorTest.txt
cn.kb.learn.testing.PasswordValidatorTest.txt
cn.kb.learn.testing.UserServiceTest.txt
TEST-cn.kb.learn.testing.OrderCalculatorTest.xml 等 XML 文件注意 LifecycleTest$PerClassLifecycle 这类文件名:嵌套测试类会单独生成一份报告。所以报告文件数可能多于"测试类个数"——这也是本项目 Tests run: 58 与报告文件数不完全对应的原因。
./mvnw -pl testing-unit test -Dtest=OrderCalculatorTest,观察 Maven 输出的阶段顺序:resources → compile → testResources → testCompile → surefire:test——这就是"生命周期"的字面含义;OrderCalculatorTest 改名为 OrderCalculatorCheck,重跑 ./mvnw -pl testing-unit test,观察 Tests run 数字变小(这个类不再被执行)——亲手体验"命名约定"这道隐形规则(完成后改回来);target/surefire-reports/*.xml,找到 <testcase> 节点,观察它记录了类名、方法名、耗时——CI 的测试报告就是解析这个文件渲染出来的;labs/pom.xml 里临时删掉 <scope>test</scope>,跑一次 ./mvnw -pl testing-unit dependency:tree,观察 JUnit 从 test 作用域变成了 compile 作用域(完成后改回来)。class OrderCalculatorCheck { } // 不以 Test 开头或结尾 → 不会被 surefire 发现症状:mvn test 显示 "Tests run: 0" 或总数比预期少,但不报错。
修复:改成 OrderCalculatorTest(或用 <includes> 显式配置匹配规则——但不建议偏离约定)。
scope=test <dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<!-- 漏了 <scope>test</scope> -->
</dependency>后果:AssertJ 被打进生产包。修复:所有测试专用依赖都加 scope=test。
排查方法:./mvnw dependency:tree -Dscope=compile 看生产依赖里有没有测试库。
*Test 命名下 class UserRepositoryIntegrationTest { } // 以 Test 结尾 → 每次 mvn test 都会跑它后果:本地没有数据库时 mvn test 直接失败,开发体验崩塌。
修复:改名为 UserRepositoryIT,并配置 failsafe 插件——让需要环境的测试只在 mvn verify 时执行。
症状:编译成功(IDE 里都能看到),但 mvn package 时报"找不到符号"。
原因:IDE 把 src/test/java 也放进了类路径,而 Maven 不会。Maven 的行为才是正确的——这也说明不要只信 IDE 的编译结果。
-DskipTests 让构建变绿 见 6.3 节。修复:找出失败原因,而不是跳过测试。
目标:把 Maven 的测试执行完全摸一遍。
步骤:
OrderCalculatorTest 临时改名为 OrderCalculatorCheck,跑完整测试,记录 Tests run 从 58 变成多少;再改回来;OrderCalculatorTest 里的一个断言失败,分别用 -Dtest=OrderCalculatorTest 与完整 mvn test 各跑一次,对比"失败时构建是否继续执行后续测试类";testing-unit/target/surefire-reports/cn.kb.learn.testing.OrderCalculatorTest.txt,找出失败用例的记录格式(提示:第 3 步之后它会有 <failure> 记录);ParameterizedOrderCalculatorTest 加 @Tag("slow"),用 -DexcludedGroups=slow 跑一次,观察它是否被排除。验收方式:能说出第 3 步两次运行的差别,以及第 5 步 -DexcludedGroups 的效果。
扩展任务:在 testing-unit 模块里加一个 UserRepositoryIT(只打印一行"集成测试执行"),配置 failsafe 插件,验证:mvn test 不跑它、mvn verify 会跑它。
基础题
src/test/java 里的类会被打进最终的 jar 包。(对 / 错)-DskipTests 与 -Dmaven.test.skip=true 的区别是什么?代码阅读题
src/
├── main/java/com/example/UserService.java
├── main/resources/application.properties
├── test/java/com/example/UserServiceTests.java
├── test/java/com/example/UserServiceIntegrationIT.java
└── test/resources/application.propertiesmvn test 会执行哪些测试类?application.properties 与生产的同名文件,哪个会被测试加载到?编程题
labs/testing-lifecycle 模块(packaging 为 jar);Calculator(加法与除法,除数为 0 抛 ArithmeticException)与对应的 CalculatorTest;mvn -pl testing-lifecycle test 能跑通;CalculatorCheck 后 Tests run 变成 0,再改回来。综合任务
*Test,快):每次 mvn test 都跑;@Tag("slow")):本地默认排除,CI 上执行;*IT,需要外部服务):只在 mvn verify 时执行;提示 1(方向):第 4 题的关键是 surefire 只认 *Test / Test* / *Tests / *TestCase,*IT 归 failsafe。
提示 2(关键点):第 6 题的 pom 只需要 parent 继承 + JUnit 依赖(scope test)——参考 labs/testing-unit/pom.xml。
提示 3(接近答案):第 7 题的核心矛盾是"反馈速度 vs 覆盖范围"。本地要快(秒级),CI 要全(分钟级),用命名约定与 tag 把两者分开。
src/test/java 只在测试编译与运行期使用,不会被打进产物。这是 Maven 的硬保证,也是"测试代码可以随便写辅助工具类"的底气。**/Test*.java、**/*Test.java、**/*Tests.java、**/*TestCase.java。-DskipTests:跳过执行,但仍编译测试代码;-Dmaven.test.skip=true:不编译也不执行。实践上优先用 -DskipTests:因为它保留了"测试代码能否编译通过"这道检查。测试代码引用了已删除的方法时,-DskipTests 会让构建失败(好事),而 -Dmaven.test.skip=true 会掩盖这个问题。
第 4 题:mvn test 只执行 UserServiceTests。
UserServiceIntegrationIT 以 IT 结尾,归 failsafe 管,只在 mvn verify 时执行。
第 5 题:测试目录下的 application.properties 会生效。
Maven 的类路径里 target/test-classes 排在 target/classes 之前,所以同名资源测试版本胜出。这是做"测试环境配置覆盖"的标准手段——不用改生产配置,也能让测试连到测试数据库。
一个常见困惑:本地跑测试通过、CI 上失败,有时候就是"某个测试配置在 CI 上没有对应文件"导致的。检查顺序:src/test/resources → src/main/resources。
<!-- labs/testing-lifecycle/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.kb.learn</groupId>
<artifactId>labs</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>testing-lifecycle</artifactId>
<name>labs-testing-lifecycle</name>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>// src/main/java/cn/kb/learn/lifecycle/Calculator.java
package cn.kb.learn.lifecycle;
public class Calculator {
public long add(long a, long b) {
return a + b;
}
public long divide(long a, long b) {
if (b == 0) {
throw new ArithmeticException("除数不能为 0");
}
return a / b;
}
}// src/test/java/cn/kb/learn/lifecycle/CalculatorTest.java
package cn.kb.learn.lifecycle;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CalculatorTest {
private final Calculator calculator = new Calculator();
@Test
@DisplayName("加法")
void adds() {
assertThat(calculator.add(2, 3)).isEqualTo(5L);
}
@Test
@DisplayName("除以 0 抛 ArithmeticException")
void rejects_division_by_zero() {
assertThatThrownBy(() -> calculator.divide(1, 0))
.isInstanceOf(ArithmeticException.class)
.hasMessage("除数不能为 0");
}
}验证:
./mvnw -pl testing-lifecycle test
# Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
# 改名成 CalculatorCheck 后:
# Tests run: 0, Failures: 0, Errors: 0, Skipped: 0注意:把类改名为 CalculatorCheck 后,构建仍然 BUILD SUCCESS——这正是"命名约定"最危险的地方:它不报错,只是安静地不执行。
三条命令清单:
# 1. 开发日常(秒级):改一行代码跑一次
./mvnw -pl testing-unit test -Dtest=OrderCalculatorTest # 单个类,最快
./mvnw -pl testing-unit test -DexcludedGroups=slow # 整个模块,排除慢测试
# 2. 提交前(十几秒到一分钟):确保不会污染主分支
./mvnw -pl testing-unit test # 全部单元测试
./mvnw -pl testing-unit test -Dgroups=slow # 慢测试也跑一遍
# 3. CI 上(几分钟):完整验证
./mvnw -B -ntp verify # 单元 + 集成测试
./mvnw -B -ntp test -Dgroups=slow # 慢测试
# 附加:覆盖率门禁(T1-018)、静态检查为什么"CI 比本地全"是合理的:
| 维度 | 本地 | CI |
|---|---|---|
| 目标 | 快速反馈(改代码 → 知道对不对) | 完整验证(合并前必须全绿) |
| 频率 | 每分钟可能多次 | 每次推送一次 |
| 成本 | 开发者等待的时间 | 机器时间(可并行、可等待) |
| 环境 | 可能缺数据库、外部服务 | 可以准备完整环境 |
核心矛盾:反馈速度与覆盖范围成反比。开发者如果每次改一行都要等 5 分钟,就会开始绕过测试(-DskipTests 满天飞),测试的实际保护力反而下降。
所以分层不是妥协,而是为了让"该跑的时候一定跑,不该跑的时候不拖累人"。这也是为什么"测试金字塔"(T1-005 测试金字塔与测试左移)强调"大量快速单元测试 + 少量慢速集成测试"——速度本身就是测试策略的一部分。
src/test/java 与 src/test/resources 的约定与作用;scope=test;-Dtest 精确指定类/方法,用 -DskipTests 临时跳过;target/surefire-reports,并知道 XML 是给 CI 用的;《JaCoCo:覆盖率指标与报告》:让测试的"执行情况"变成可量化的数字。
暂无