1. 为什么Spring Boot项目测试总在“版本”上栽跟头
不管你是刚把第一个接口写完的新手,还是已经维护了几年老项目的资深开发者,只要在Spring Boot里写过单元测试,基本都遇到过这么几种情况:
依赖照着网上的教程加了一堆,启动测试时报NoSuchMethodError或者ClassNotFoundException;本来@Test注解用得挺好,升级一下Spring Boot版本,所有测试突然全红了;还有更隐蔽的——测试能跑通,但MockMvc一直注入不进来,或者@Transactional根本不回滚,查了半天才发现是JUnit版本和Spring测试支持之间没配对。
这些问题归根结底指向同一个核心:Spring Boot对JUnit 4和JUnit 5的支持方式完全不一样,而大多数教程根本没说清楚这两条路该怎么选、怎么配。我在帮朋友排查项目问题时见过太多同类情况:pom里同时出现junit:junit和org.junit.jupiter:junit-jupiter,两个版本的注解混着用,旧的@RunWith和新的@ExtendWith叠在一起,项目能编译但测试行为完全不可控。
这篇文章以“Spring Boot中使用JUnit 5/JUnit 4”为主线,把两条路的依赖配置、注解差异、版本兼容、常见坑位全盘梳理一遍。适合这几类读者:正在从JUnit 4迁到JUnit 5的老项目维护者;新开Spring Boot项目不想在测试依赖上浪费时间的开发者;以及像我一样,排查半天发现是版本问题导致的测试异常,想一次搞清楚底层规则的排查党。
我先说结论方便你有个框架:Spring Boot 2.2之后默认支持JUnit 5,但工程里如果还在用老一套的spring-boot-starter-test,它默认带进来的其实是JUnit 4兼容层;Spring Boot 2.4之后,依赖里默认的测试引擎变成了JUnit 5,但junit-vintage-engine这个JUnit 4兼容引擎默认不再包含。这个“默认”的变化,就是大部分混乱的源头。
下面我按“选型 -> 配置 -> 能力 -> 踩坑”的顺序来拆。
2. JUnit 5和JUnit 4到底差在哪:不是升级,是重构
很多人把JUnit 5当成JUnit 4的加量版,觉得无非就是注解名字换了一下、多几个断言方法。这个理解会直接导致你在实际接入时踩坑。JUnit 5本质上是一次彻底的架构重构,它把原来JUnit 4那个大而全的框架拆成了三个独立模块,各管一摊:
- JUnit Platform:在JVM上启动测试框架的基础,也是测试引擎(TestEngine)的运行入口。这一步是JUnit 5区别于JUnit 4最核心的架构变化,它允许不同测试框架在同一个平台上运行。
- JUnit Jupiter:我们写测试用的注解(
@Test、@BeforeEach、@DisplayName)、断言、测试引擎逻辑都在这里,它是在Platform之上实现的具体编程模型。 - JUnit Vintage:一个兼容引擎,用来在JUnit Platform上跑JUnit 3/JUnit 4的老测试。
这个拆分的价值不仅仅是“更干净”,而是生态层面的。Spring Boot里的spring-test模块可以只依赖Platform抽象,不需要绑定具体某个版本的JUnit;Mockito、AssertJ这些第三方库也能各自适配;IDE和构建工具只需要理解Platform一套标准。这也是为什么Spring Boot能把测试支持演进得这么快——它不需要跟着某个具体框架版本走,只要适配Platform接口就行。
JUnit 4只有一个junit:junit包,所有能力混在一起。好处是老项目只要引一个依赖就能跑,坏处是需要扩展能力时,JUnit 4只能通过@RunWith和@Rule这两个钩子去做,很多高级场景(比如条件执行、动态测试、参数扩展)根本塞不进去。JUnit 5直接用一组清晰的SPI接口(如TestExecutionListener、ParameterResolver、TestTemplateInvocationContextProvider)替代了那个万能的@RunWith,从框架设计上说,扩展点更可控、更类型安全。
给你一个特别直观的类比:JUnit 4像一台老式一体机,打印、扫描、复印都在一个盒子里,要加功能只能往USB口上插U盘;JUnit 5像把打印机、扫描仪、复印机拆成了独立设备,用统一接口连到一个办公平台上。Spring Boot对测试的编排能力,就建立在这套统一接口上。
理解了这一层,你再看Spring Boot的依赖变化就不会迷糊了。Spring Boot 2.1及之前的spring-boot-starter-test里包含的是JUnit 4,你用@RunWith(SpringRunner.class)是对的;Spring Boot 2.2到2.3的starter里引入了JUnit 5的依赖,但同时也保留了JUnit 4的兼容引擎,这一阶段你可以两个注解混用;Spring Boot 2.4开始,starter的默认测试能力迁到了JUnit 5,如果你还需要跑老测试,必须手动加junit-vintage-engine。
这也是为什么“网上抄来的配置跑不通”的概率这么大——因为你看到的教程可能是两年前的,而你用的Spring Boot版本已经是三年前的设计了。这不是文章写错了,是版本演进把配置从“默认可用”改成了“需要显式声明”。
3. Spring Boot项目里两条路线的接入实操
开始配置之前,先要明确你当前项目的Spring Boot版本,这决定了“最小配置”长什么样。我建议的做法是:
先看
pom.xml里的parent或者dependencyManagement中的Spring Boot版本号。如果是2.4及以上,直接按JUnit 5的方案走;如果是2.3及以下,看现有代码是否已经在用@RunWith、@Before、@After这些老API,有的话先按JUnit 4方案理清楚,再规划迁移。
3.1 路线A:Spring Boot + JUnit 4(老项目维护场景)
虽然JUnit 5已经是很成熟的选择,但现实情况是,很多老项目还跑在JUnit 4上,有些是历史包袱太重改不动,有些是团队还没计划迁移。如果你要在一个Spring Boot 2.4以上的新工程里强制用JUnit 4,也不是不行,但要手动排除JUnit 5相关依赖。
以Spring Boot 2.7.x为例,最直接的JUnit 4配置是这样:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency>注意第二段没有写版本号。这是Spring Boot的dependencyManagement在起作用——Boot的BOM里已经锁定了JUnit 4的版本,你不需要也不能手动指定,否则可能在升级Spring Boot时造成版本不匹配。
写测试的时候,Spring Boot 2.x对JUnit 4的入口是@RunWith(SpringRunner.class):
@RunWith(SpringRunner.class) @SpringBootTest public class UserServiceTest { @Autowired private UserService userService; @Before public void setUp() { // 每个测试方法执行前初始化 } @After public void tearDown() { // 每个测试方法执行后清理 } @Test public void testGetUserById() { User user = userService.getUserById(1001L); Assert.assertNotNull(user); Assert.assertEquals("张三", user.getName()); } }如果你是Spring Boot 3.x,我不建议再折腾JUnit 4了。Boot 3是基于Jakarta EE 9+构建的,对JUnit 4的支持已经完全舍弃,硬要配的话需要极老版本的Spring Test和一系列补丁依赖,真要走这条路还不如直接规划迁移。
3.2 路线B:Spring Boot + JUnit 5(新项目推荐路径)
JUnit 5在Spring Boot 2.4以上的配置非常简单,尤其当你用2.4以后的版本时,spring-boot-starter-test已经把JUnit 5(junit-jupiter)作为默认引擎。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>就这一条。它的传递依赖里已经包含了:
org.junit.jupiter:junit-jupiter(JUnit 5核心,包括API、参数化测试和引擎)org.mockito:mockito-junit-jupiter(Mockito对JUnit 5的集成)org.assertj:assertj-core(流式断言库)org.springframework:spring-test(Spring测试支持)org.springframework.boot:spring-boot-test(Boot层的测试支持,如@SpringBootTest)com.jayway.jsonpath:json-path等辅助库
写测试时,用JUnit 5的这套写法:
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class UserServiceTest { @Autowired private UserService userService; @BeforeEach void setUp() { // JUnit 5的初始化方法:不需要public,方法包级私有即可 } @Test void testGetUserById() { User user = userService.getUserById(1001L); Assertions.assertNotNull(user); Assertions.assertEquals("张三", user.getName()); } }看到区别了吗?@RunWith(SpringRunner.class)在JUnit 5里不需要了——Spring通过@ExtendWith(SpringExtension.class)来接入JUnit 5,但@SpringBootTest这个注解在Boot 1.5之后自己就带上了@ExtendWith(SpringExtension.class)的元注解,所以直接用就行。这是一个非常容易被忽略的细节,很多从JUnit 4转过来的同学还在到处找@ExtendWith的配置。
3.3 版本对照速查表
| Spring Boot版本 | Starter默认测试引擎 | 配JUnit 4的复杂度 | 配JUnit 5的复杂度 | 推荐选择 |
|---|---|---|---|---|
| 2.1及以下 | JUnit 4 | 直接可用 | 需要手动加依赖并排除旧包 | JUnit 4 |
| 2.2 - 2.3 | JUnit 5 + Vintage兼容 | 直接可用 | 直接可用 | 项目新建用JUnit 5 |
| 2.4 - 2.6 | JUnit 5(无Vintage) | 需手动加junit:junit并排除jupiter | 直接可用 | JUnit 5 |
| 2.7 - 3.x | JUnit 5(无Vintage) | 不推荐 | 直接可用 | JUnit 5 |
这个表是我实际配置大量项目后的总结,核心判断逻辑很简单:新项目无脑JUnit 5,老项目如果代码在2.4之前诞生、且测试数量大,短期保留JUnit 4没问题,但新写的测试尽量切到JUnit 5风格,为后续迁移逐步铺路。
4. JUnit 5真正能用起来的高级能力
JUnit 5绝不只是换了个包名。至少在Spring Boot项目里,下面这些能力是JUnit 4很难优雅实现的,而JUnit 5只是加了注解或参数就搞定。
4.1@DisplayName:中文测试名,可读性直接拉满
JUnit 4里没这个注解,你只能靠testXxx()这样的方法名硬撑,看的多了眼睛干。JUnit 5直接用字符串描述测试意图:
@DisplayName("用户服务 - 查询用户信息") class UserServiceTest { @Test @DisplayName("根据用户ID查询用户,返回完整的用户信息") void testGetUserById() { // ... } }IDEA和Maven Surefire的报告里显示的都是这个@DisplayName,而不是方法名。测试报告的可读性提升,对业务方汇报也友好很多。
4.2 参数化测试:JUnit 5的王牌功能
JUnit 4里要做“同一套逻辑跑多组数据”的话,要么写循环、要么定制@RunWith(Parameterized.class)——后者非常繁琐,需要专门的类构造器配合。JUnit 5把这个能力做成了参数化测试,最简单的用法:
@ParameterizedTest @ValueSource(strings = { "张三", "李四", "王五" }) void testFindUsersByName(String name) { List<User> users = userService.findUsers(name); Assertions.assertTrue(users.stream().allMatch(u -> u.getName().equals(name))); }还有更强大的@CsvSource,直接在注解里写多组数据,省去大量手写数据集工厂的样板代码:
@ParameterizedTest @CsvSource({ "admin, true", "user, false", "guest, true" }) void testCheckPermission(String role, boolean expected) { boolean result = permissionService.check(role); Assertions.assertEquals(expected, result); }Spring项目里配合@MethodSource有个技巧很实用——可以用一个静态方法动态生成测试数据,甚至从数据库或接口响应里取数据来喂给测试方法,这让“根据线上真实数据回放做测试”成为了可能。
4.3 嵌套测试:给测试内部建结构化目录
JUnit 5的@Nested注解允许在测试类内部构造层级:
@SpringBootTest @DisplayName("订单模块测试") class OrderServiceTest { @Nested @DisplayName("创建订单相关") class CreateOrderTests { @Test @DisplayName("创建订单成功时返回订单号") void testCreateSuccess() { } } @Nested @DisplayName("取消订单相关") class CancelOrderTests { @Test @DisplayName("取消不存在订单时抛出异常") void testCancelNotFound() { } } }测试报告会按嵌套层级展示,层次更清晰。尤其在业务模块测试方法非常多的时候,这个能力能把“哪部分出问题了”这件事从一堆平铺的方法名里跳出来,报告扫一眼就知道是哪个业务子模块挂了。
4.4@RepeatedTest与动态测试:覆盖特殊场景
@RepeatedTest(5)可以让一个测试重复执行5次,适合稳定性和并发相关的冒烟验证;动态测试@TestFactory更进一步,是在运行时生成测试用例的方法,常用于数据驱动场景。虽然这两个用到的频次不高,但遇到随机性问题、并发竞态问题时,能少写很多蹩脚的循环代码。
4.5 条件测试:按环境跳过或停用
这个在实际项目里超级实用。比如某些测试只希望在CI环境跑,某些涉及外部接口的测试本地能跑但CI没网络就不应该跑:
@Test @EnabledIfSystemProperty(named = "test.env", matches = "ci") void testOnlyInCI() { } @Test @DisabledIfEnvironmentVariable(named = "SKIP_INTEGRATION", matches = "true") void testIntegration() { }JUnit 5还提供了@Timeout注解,可以给测试用例设置超时时间,避免数据库连接池耗尽导致测试挂几分钟才报错。这些都是JUnit 4里很难用配置化方式做到的。
这些能力组合起来意味着什么?你可以在同一个测试类里,既有纯单元测试(Mock掉所有外部依赖)、又有条件跳过的集成测试、还有参数化跑的数据校验,一套结构能覆盖绝大部分测试需求。
5. Spring Boot测试的依赖版本管理:为什么有的包不用写版本号
很多新人在配置Spring Boot测试依赖时会有一个困惑:为什么spring-boot-starter-test里的传递依赖不用写版本号?为什么我自己引入junit-jupiter的时候不写版本号也能编译过?
原因在Spring Boot的统一依赖管理机制上。Spring Boot的父POM(spring-boot-dependencies)用<dependencyManagement>定义了一整套经过兼容性测试的依赖版本。你引入一个Spring Boot管理的依赖而不指定版本时,Maven就会去这个清单里找匹配的版本;你在IDE里看到的实际解析版本,就是BOM里锁定的那个。
这是一个特别值得强调的实践原则:Spring Boot项目的测试依赖,不要手动指定版本号。只有这个包不在Boot的BOM管理范围内(比如你自己引入的第三方库),才需要自己声明版本。
看一个反面案例。同一个项目里,有的人在pom里写了:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.7.2</version> <scope>test</scope> </dependency>而Spring Boot 2.7默认管理的JUnit Jupiter版本可能是5.8.x。一旦某个测试只有在新版本才有修复的bug,就会因为手动锁定旧版本而出现诡异行为。更严重的是,如果手动指定的版本和Spring Boot中的其他组件(比如Mockito、Spring Test)不兼容,升级时会直接让你排查到崩溃。
所以我的建议就一条:项目依赖走Boot BOM,不要去手动覆盖版本,除非你完全清楚自己在干什么,并且有明确的兼容性测试结果。这也是为什么我在前面给的配置示例里,JUnit 4和JUnit 5的依赖都没有version——Boot都替你管了。
补充一点,如果你是用Gradle来做依赖管理,Spring Boot插件同样提供dependency-management插件来统一版本。逻辑一致,不再赘述。
6. 从JUnit 4迁到JUnit 5的完整映射与动作清单
如果你负责的老项目已经在用JUnit 4,想开始迁到JUnit 5,不用一次性把几百个测试全改了。我建议分步骤做,每步都能独立编译运行:
第一步,在pom里引入junit-vintage-engine,确保老测试继续跑通,这一步是为了让构建不红:
<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> </dependency>第二步,新写的测试全部用JUnit 5注解。Java包命名是天然的隔离——JUnit 4的注解都在org.junit包下,JUnit 5的注解都在org.junit.jupiter.api包下。你可以混用,但注意一个类里不要同时用两个版本的@Test,否则有些Runner会只执行其中一个。
第三步,按映射表逐类迁移:
| JUnit 4 | JUnit 5 | 说明 |
|---|---|---|
@Before | @BeforeEach | 每个方法前执行 |
@After | @AfterEach | 每个方法后执行 |
@BeforeClass | @BeforeAll | 类加载后执行一次,JUnit 5要求方法static |
@AfterClass | @AfterAll | 所有方法结束后执行一次,JUnit 5要求方法static |
@Test(expected = XxxException.class) | Assertions.assertThrows(XxxException.class, () -> ...) | 断言异常方式变了 |
@Test(timeout = 1000) | @Timeout(1)或Assertions.assertTimeout(...) | 超时配置方式变了 |
@Ignore | @Disabled | 停用测试 |
@RunWith(SpringRunner.class) | 不需要,@SpringBootTest自带@ExtendWith(SpringExtension.class) | 启动Spring容器的方式 |
@RunWith(MockitoJUnitRunner.class) | @ExtendWith(MockitoExtension.class) | Mockito与JUnit 5的集成 |
Assert.assertEquals | Assertions.assertEquals | 断言类变更,推荐静态导入 |
org.junit.Test | org.junit.jupiter.api.Test | 包名变更 |
第四步,SQL初始化相关方法从@Before里迁移时,注意@BeforeAll只能用static方法。如果你在JUnit 4里用了@BeforeClass里初始化数据库,JUnit 5要写成:
@BeforeAll static void initDatabase() { // 静态方法才能调用 }第五步,删掉junit-vintage-engine,强制你的测试类全部运行在JUnit 5上。这时如果有遗漏的老API,构建会直接报错,报什么就补什么,比人为排查准确得多。
这套流程我实操过几轮,关键点是第二步和第三步不要合并做——不要“一边写新注解一边删老依赖”,那样出错后根本分不清是迁移问题还是依赖问题。
7. 实际项目里的踩坑实录与问题速查
7.1 Mockito和JUnit 5的“兼容性陷阱”
老项目里经常能看到这种写法:
@RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserMapper userMapper; }切到JUnit 5后,@RunWith失效,而且mockito-junit-jupiter这个桥接包如果没加进依赖,@Mock注解根本不会生效——你的mock对象是null,调用时直接NPE。正确的JUnit 5写法是:
@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserMapper userMapper; }同时确保pom里有mockito-junit-jupiter(Spring Boot的starter在2.2之后默认带)。这个包的作用就是把Mockito的生命周期挂接到JUnit 5的扩展模型上,没有它,@Mock就是普通的注解,什么都不做。
7.2@Transactional不回滚:测试数据污染库
在Spring Boot集成测试里,代码长这样:
@SpringBootTest @Transactional void testCreateUser() { userService.createUser(...); }这个设计是想利用事务回滚,让测试执行完数据自动还原。但它有个隐含前提——测试类所在的线程和事务管理线程必须一致,而且测试引擎必须支持Spring的测试事务管理。如果你把测试切到JUnit 5但忘了测试类里有@RunWith(SpringJUnit4ClassRunner.class)相关配置,事务管理器可能根本没被挂载上去,导致数据写进了库,测试看着“通过”了,DB里垃圾数据越来越多。
排查方法是看日志里有没有Rolled back transaction for test这种提示;没有的话,检查@SpringBootTest是否生效、测试类是否还在被某些Runner劫持。
7.3 Surefire插件版本与JUnit 5:测试“跑了个寂寞”
有些项目有专门的maven-surefire-plugin配置。JUnit 5项目里如果你把Surefire强制设成旧版本(比如2.12),Maven可能压根不执行任何JUnit 5测试,但构建还是显示成功——因为Surefire的2.x版本只能识别JUnit 4的测试,JUnit 5的测试类在它眼里不存在。这比测试报错更坑:你以为自己在写测试,其实Maven根本没跑过它们。
解决方案是确保Surefire版本在2.22.0以上(这个版本开始支持JUnit Platform)。Spring Boot 2.4以上默认内置的是3.x版本,一般不用改。Gradle用户注意useJUnitPlatform()要在test任务里显式开启,老项目经常忘了加,同样会出现“测试没执行”的假象。
7.4No tests found但是明明有测试类
这个报错通常发生在IDE里直接运行测试类时。原因一般是测试类没有遵循“公共无参构造器 + 测试方法不是static”的约定,或是因为构建工具没有正确发现测试。排查思路按顺序:
- 确认类名不是以
Test结尾也没关系,关键是方法上有@Test注解; - 确认类不是
abstract的,JUnit 5不能实例化抽象测试类; - 如果类定义在package-private下,JUnit 5完全支持(JUnit 4要求public);
- 检查Maven的
testIncludes配置是否限制了**/*Test.java之类的通配符。
我给这类问题一个优先解:先在IDE里对着某个测试方法右键执行,如果能跑通,说明测试代码没问题,问题出在构建配置;如果IDE也报No tests found,看注解包名是否引错了。
7.5 版本冲突:ClassNotFoundException 与 NoSuchMethodError
最后聊一个最“经典”的报错组合。类的NoClassDefFoundError、运行期的NoSuchMethodError,基本都指向:你的classpath里有多个不同版本的JUnit依赖。比如同时解析到了junit:junit:4.12和org.junit.jupiter:junit-jupiter-api:5.4.2,而代码里用了两个时代的注解。
用Maven排查命令看依赖树是最直接的:
mvn dependency:tree -Dincludes=junitGradle用:
gradle dependencies --configuration testRuntimeClasspath看到同一groupId出现多个版本,用<exclusion>排除不需要的。另一个高频坑是:有的中间依赖会把JUnit 4带进运行时,即使你自己只加了JUnit 5,最后运行测试时加载了JUnit 4的类,然后所有用org.junit.jupiter.api.Test注解的类全部无法识别。这种情况下,构建配置里的传递依赖必须做一次彻底清洗。
8. 项目里测试启动的最终检查清单
每次新建Spring Boot项目或者接手新项目,我都建议按这份清单过一遍测试环境,能省去后面大量的“为什么跑不通”排查时间:
- [ ] pom或build文件里只有一条测试starter:
spring-boot-starter-test(scope=test) - [ ] 不要手动指定JUnit相关版本号,让Boot的BOM统一管理
- [ ] JUnit 4时代的
@RunWith全部替换为JUnit 5的@ExtendWith或不写(@SpringBootTest自带) - [ ]
@Before/@After按对应关系换成@BeforeEach/@AfterEach,@BeforeClass/@AfterClass换成@BeforeAll/@AfterAll且方法用static - [ ]
Assert.*换成Assertions.*并静态导入 - [ ] Mockito测试用
@ExtendWith(MockitoExtension.class) - [ ] 如果项目里还需要跑JUnit 4测试,显式引入
junit-vintage-engine - [ ] Maven项目的Surefire不低于2.22.0,Gradle项目的test任务里加了
useJUnitPlatform() - [ ] 执行
mvn test后确认日志里能看到Tests run: X, Failures: Y, Errors: Z,并且X不为0——这能避免“测试根本没跑却在读绿色构建”的假象
9. 最后说几个我的实际体会
Spring Boot的测试栈里,JUnit 5和JUnit 4的切换本质上不是“升级依赖”这么简单的事。它牵扯到Spring Test对测试框架的接入方式、Mockito的扩展机制、Maven/Gradle的构建识别逻辑,每一层都有各自的版本适配条件。我碰过太多次“改了注解但没跑起来”的案例,最后发现都卡在依赖传递或Surefire版本上。
如果让我给一个最省心的实践方案:新项目直接用Spring Boot 2.4以上版本 + 默认starter + JUnit 5编写测试,不要回头看JUnit 4;老项目迁不动的时候,在spring-boot-starter-test旁边加junit-vintage-engine,让新旧测试共存过渡——这是投入产出比最高的方案。
另外有两个小技巧在实际调测中非常有用。一是给Spring Boot的测试类加上@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)来模拟真实HTTP调用,配合TestRestTemplate或MockMvc做端到端验证;二是当某个测试方法需要特定Bean但不想动用整个Spring上下文时,可以混用@MockBean和JUnit 5的@ExtendWith,局部替换依赖,而不是整容器启停。测试写得约快、反馈越及时,工程交付的质量也越可靠。