一、junit4与junit5及springboot中的使用
在现代软件开发中,单元测试是确保代码质量的重要环节。spring boot框架通过整合junit,为开发者提供了便捷的单元测试支持。
1.1 spring boot中junit版本的变化
在spring boot 2.0之前,框架默认使用junit 4作为测试平台。然而,从spring boot 2.0开始,junit 5成为默认的测试框架。以下是spring boot不同版本中junit版本的对比:
| spring boot版本 | 默认junit版本 |
|---|---|
| 1.x | junit 4 |
| 2.x | junit 5 |
例如,spring boot 2.2.0使用junit 5.5.2版本。开发者可以通过pom文件确认具体版本。
1.2 pom文件配置
在spring boot项目中,单元测试的依赖通过spring-boot-starter-test启动器引入。以下是pom文件的配置示例:
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-test</artifactid>
<scope>test</scope>
<exclusions>
<exclusion>
<groupid>org.junit.vintage</groupid>
<artifactid>junit-vintage-engine</artifactid>
</exclusion>
</exclusions>
</dependency>1.3 排除junit vintage engine
junit-vintage-engine是junit 3和junit 4的运行支持平台。默认情况下,spring boot测试启动器会排除该依赖,以鼓励开发者使用junit 5。如果需要使用junit 4,可以移除<exclusions>标签。
二、junit 4与junit 5的对比
以下是junit 4和junit 5的主要差异:
| 特性 | junit 4 | junit 5 |
|---|---|---|
| 注解 | @runwith、@test | @springboottest、@test |
| 默认启动类支持 | 需要手动指定启动类 | 自动检测启动类 |
| 测试方法支持 | 需要@test注解 | 需要@test注解 |
| 扩展支持 | 有限 | 更强大的扩展机制 |
三、junit 4测试代码示例
以下是一个基于junit 4的测试代码示例:
import org.junit.runner.runwith;
import org.springframework.boot.test.context.springboottest;
import org.springframework.test.context.junit4.springjunit4classrunner;
import org.junit.test;
@runwith(springjunit4classrunner.class)
@springboottest(classes = springbootexceptionandjourneyapplication.class)
public class userservicetest {
@test
public void testadduser() {
system.out.println("junit 4 测试方法运行成功!");
}
}四、junit 5测试代码示例
以下是一个基于junit 5的测试代码示例:
import org.springframework.boot.test.context.springboottest;
import org.junit.jupiter.api.test;
@springboottest
public class userservicetest {
@test
public void testadduser() {
system.out.println("junit 5 测试方法运行成功!");
}
}
五、实际案例:持久层与业务层测试
假设我们有一个userdao和userservice,以下是它们的实现代码:
5.1 持久层代码
public class userdaoimpl {
public void insert() {
system.out.println("insert into user values(...)");
}
}5.2 业务层代码
import org.springframework.stereotype.service;
@service
public class userserviceimpl {
private final userdaoimpl userdao;
public userserviceimpl(userdaoimpl userdao) {
this.userdao = userdao;
}
public void adduser() {
userdao.insert();
}
}5.3 测试代码
import org.springframework.boot.test.context.springboottest;
import org.junit.jupiter.api.test;
import org.springframework.beans.factory.annotation.autowired;
@springboottest
public class userservicetest {
@autowired
private userserviceimpl userservice;
@test
public void testadduser() {
userservice.adduser();
}
}六、常见问题与解答
以下是关于spring boot整合junit单元测试的常见问题:
| 问题 | 答案 |
|---|---|
| 1. spring boot默认使用哪个junit版本? | spring boot 2.x默认使用junit 5。 |
| 2. 如何在spring boot中使用junit 4? | 需要移除pom文件中junit-vintage-engine的排除配置。 |
| 3. junit 5是否需要指定启动类? | 默认情况下,junit 5会自动检测启动类。如果项目中有多个启动类,需要通过@springboottest(classes = ...)指定。 |
| 4. junit 5的测试方法是否需要@runwith注解? | 不需要,junit 5使用@springboottest注解即可。 |
| 5. 如何确认junit版本? | 通过pom文件中的依赖版本确认,例如junit-jupiter-api的版本号。 |
七、流程图:spring boot整合junit单元测试的步骤
创建spring boot项目添加spring-boot-starter-test依赖确认junit版本编写测试代码运行测试

第一部分总结
我们现在直接使用junit5在springboot项目中使用即可。使用方式为添加注解@springboottest(会自己找启动类,有多个则通过classes = ...指定)就可以让容器启动来测试。
疑问1:不是说5的话boot项目需要ext注解吗?
ai解答: @extendwith 确实是 junit 5 的核心注解。
不过,在 spring boot 项目里,你通常不需要手动添加它。
🤔 为什么会有这个误解?
你的理解是对的:junit 5 确实使用 @extendwith 来集成 spring 的功能,它取代了 junit 4 的 @runwith。
在早期版本的 spring boot (2.1.x 之前),确实需要手动在测试类上写上 @extendwith(springextension.class)。
✅ 现在的实际情况是怎样的?
从 spring boot 2.1.x 版本开始,官方为了让开发更便捷,做了一个“贴心”的优化。
@springboottest 这个我们最常用的注解,其内部源码已经自动包含了 @extendwith(springextension.class)。
你可以把它理解为:@springboottest 是一个“大礼包”,里面已经帮你把 @extendwith 这个“工具”放进去了。所以,当你使用 @springboottest 时,就无需再重复添加了。
代码对比
早期写法 (已过时)
// 在 spring boot 2.1 之前,需要这样写 @extendwith(springextension.class) // 手动启用 spring 扩展 @springboottest public class orderservicetest { // ... }现在写法 (推荐) // 现在,这样写就足够了! @springboottest public class orderservicetest { // ... }
💡 什么时候才需要手动写@extendwith?
只有在一些不使用 @springboottest 的底层测试场景中,你才可能需要手动添加。
例如,如果你只用 @contextconfiguration 来加载一个特定的 spring 配置文件进行测试,这时就需要手动通过 @extendwith(springextension.class) 来启用 spring 的扩展支持。
但对于绝大多数 spring boot 的集成测试来说,放心地只使用 @springboottest 吧!
疑问2:有必要每次启动整个容器吗?请看第二部分
第二部分 springboot项目怎么进行mock
很多开发者在刚开始写测试时,都会纠结是“启动整个 spring 容器”还是“只测一个类”。
简单来说, “优雅”的测试 = 选对测试类型 + 用好 mock 隔离
🤔 一、mock 是啥?解决啥问题?
1. 通俗解释:什么是 mock?
想象一下,你是一名汽车引擎设计师(你在测试 userservice),你需要测试引擎(userservice)的运转是否良好。
如果不使用 mock:你必须把引擎装到整辆车里,连上油箱、排气管、轮胎,甚至要把车开到路上(启动 spring 容器、连接真实数据库、连接真实 redis)。这非常慢,而且如果车打不着火,你不知道是引擎坏了,还是油箱漏了,还是轮胎没气。
使用 mock:你在实验室里,给引擎接上一个模拟油箱(mock repository)和一个模拟排气管(mock emailservice)。
- 你可以控制“模拟油箱”里有多少油(stubbing:预设返回值)。
- 你可以观察引擎是否真的向“模拟排气管”排气了(verification:验证调用)。
- 重点:你只测试引擎本身,不关心外面的世界。
2. mock 解决了什么问题?
- 速度极快:不需要启动 spring 容器,不需要连接数据库(io 操作最耗时)。单元测试通常是毫秒级的。
- 隔离性强:如果测试失败了,肯定是你的
service逻辑写错了,而不是因为数据库连不上,或者网络波动。 - 覆盖极端情况:你可以轻松模拟“数据库挂了”或者“查不到数据”的场景,而在真实环境中很难故意制造这些故障。
✨ 二、如何“优雅”地执行 boot 项目单元测试?
在 spring boot 中,优雅的核心在于 “各司其职” 。不要把所有测试都写成 @springboottest(启动全容器),那样太慢了。
我们需要区分两种测试策略:
1. 纯单元测试 (unit test) —— 推荐用于 service 层
特点:完全不启动 spring 容器,纯 java 代码运行。
工具:junit 5 + mockito (@mock, @injectmocks)。
场景:测试 userservice 里的业务逻辑(比如计算价格、校验参数)。
代码示例:
// 1. 不需要 @springboottest,不需要启动容器!
// 使用 mockito 的扩展来初始化 mock 对象
@extendwith(mockitoextension.class)
class userservicetest {
// 2. @mock: 创建一个假的 userrepository,它是空的,需要你喂数据
@mock
private userrepository userrepository;
// 3. @injectmocks: 创建 userservice 实例,并把上面的 userrepository 塞进去
@injectmocks
private userservice userservice;
@test
void shouldfinduserbyid() {
// --- arrange (准备) ---
user mockuser = new user(1l, "alice");
// 告诉 mock 对象:当有人调用 findbyid(1l) 时,返回 mockuser
when(userrepository.findbyid(1l)).thenreturn(optional.of(mockuser));
// --- act (执行) ---
user result = userservice.findbyid(1l);
// --- assert (断言) ---
assertthat(result.getname()).isequalto("alice");
// --- verify (验证) ---
// 验证 userrepository.findbyid 是否真的被调用了一次
verify(userrepository, times(1)).findbyid(1l);
}
}优雅点:速度飞快,完全隔离。
2. 切片测试 / 集成测试 (slice test) —— 推荐用于 controller 或 repository
特点:只启动 spring 容器的一部分(比如只启动 web 层,或者只启动 jpa 层)。
工具:@webmvctest (控制器), @datajpatest (数据库), @mockbean。
场景:测试 usercontroller 的接口映射是否正确,或者测试 sql 语句是否正确。
代码示例 (测试 controller) :
// 1. @webmvctest: 只启动 web 层相关的 bean (controller, converter 等),不启动 service
@webmvctest(usercontroller.class)
class usercontrollertest {
@autowired
private mockmvc mockmvc; // spring 提供的模拟 http 客户端
// 2. @mockbean: 这是 spring 的注解!
// 它会去 spring 容器里,把 userservice 替换成一个 mock 对象
@mockbean
private userservice userservice;
@test
void shouldreturnuserjson() throws exception {
// --- arrange ---
// 模拟 service 层返回数据
when(userservice.findbyid(1l)).thenreturn(new user(1l, "alice"));
// --- act & assert ---
// 发送一个模拟的 get 请求
mockmvc.perform(get("/users/1"))
.andexpect(status().isok()) // 期望状态码 200
.andexpect(jsonpath("$.name").value("alice")); // 期望返回 json 中有 name: alice
}
}优雅点:比 @springboottest 快,但又能测试 spring 的注解(如 @restcontroller, @requestmapping)是否生效。
📊 三、总结:mock 注解对比表
这是最容易混淆的地方,请注意区分:
表格
| 特性 | @mock(mockito) | @mockbean(spring boot) |
|---|---|---|
| 所属库 | mockito | spring boot test |
| 是否启动 spring | 否 (纯单元测试) | 是 (集成测试/切片测试) |
| 作用范围 | 仅在测试类内部有效 | 会替换 spring 容器中的 bean |
| 使用场景 | 测试 service 业务逻辑 | 测试 controller, 或者需要 spring 注入的场景 |
| 性能 | 极快 (毫秒级) | 较快 (秒级,取决于加载多少组件) |
🚀 四、最佳实践建议
- service 层:优先使用 @extendwith(mockitoextension.class) + @mock。不要动不动就 @springboottest,那样太慢了。
- controller 层:使用 @webmvctest + @mockbean。
- repository 层:使用 @datajpatest (它会自动配置内存数据库 h2)。
- 全链路测试:只有当你需要测试“整个应用能不能跑起来”或者“配置类是否正确”时,才使用 @springboottest。
这样分层测试,你的项目构建速度会非常快,而且逻辑清晰,维护起来也很优雅。
第二部分总结
使用mock可以最小化范围测试,而不是启动整个容器。一般测试的都是service层,直接使用@extendwith(mockitoextension.class) + @mock。不要动不动就 @springboottest,那样太慢了
第三部分 mock中的常见问题
一、mock原理
就是 “伪造” 依赖接口 / 对象 / 函数的返回结果,让程序在没有真实后端、真实服务时也能正常跑、正常测。原理为通过动态代理、字节码增强或请求拦截等方式,劫持目标方法 / 接口调用,跳过真实逻辑执行并直接返回预设伪造数据,从而实现依赖隔离与行为模拟。
✨ 疑问:final类怎么模拟呢?
在 java 中,final 关键字的设计初衷就是为了防止继承(类)或重写(方法)。而 mockito 的核心原理恰恰是生成子类(动态代理)来拦截方法调用。 所以,默认情况下,mockito 无法 mock final 类或 final 方法。如果你强行去 mock,通常会报 cannot mock/spy class ... final class 的错误。
🚀 方案:使用mockito-inline(推荐,现代做法)
这是目前最主流的做法。从 mockito 2.x 后期版本开始,官方提供了一个扩展模块 mockito-inline,它利用 java instrumentation api 在运行时修改字节码,从而支持 mock final 类。
适用场景:spring boot 2.x (较新版本) 或 spring boot 3.x,且你不想引入沉重的 powermock。
1. 添加依赖
虽然 spring boot 的 spring-boot-starter-test 已经包含了 mockito-core,但你需要额外引入 mockito-inline。
<dependency>
<groupid>org.mockito</groupid>
<artifactid>mockito-inline</artifactid>
<version>5.x.x</version> <!-- 版本号通常与 mockito-core 保持一致 -->
<scope>test</scope>
</dependency>2. 开启配置(关键步骤)
仅仅加依赖是不够的,你必须告诉 mockito 使用这个“内联”模式。
在 src/test/resources 目录下创建一个文件夹 mockito-extensions,并在其中创建一个文件 org.mockito.plugins.mockmaker。
文件路径:src/test/resources/mockito-extensions/org.mockito.plugins.mockmaker
文件内容:
mock-maker=inline

3. 编写测试
配置好后,你就可以像 mock 普通类一样 mock final 类了,代码完全不用变:
// 假设 finalservice 是一个 final 类
final class finalservice {
public string sayhello() { return "hello"; }
}
@extendwith(mockitoextension.class)
class finalservicetest {
@mock
private finalservice finalservice; // 直接 @mock,不会报错!
@test
void testfinalclass() {
when(finalservice.sayhello()).thenreturn("mocked hello");
assertequals("mocked hello", finalservice.sayhello());
}
}二、🤔 spy 是啥?解决什么问题?
1. 核心概念:部分模拟
mock(完全模拟) :创建一个空壳对象。所有方法默认都不执行真实代码,直接返回 null 或 0。你必须手动定义每一个方法的行为。
spy(部分模拟) :包装一个真实的对象。
- 默认情况下,它会执行真实的代码。
- 只有当你明确告诉它“这个方法要拦截”时,它才会返回假数据。
2. 解决什么问题?
- 场景一:遗留代码或复杂对象。当你有一个类,方法很多,你只想 mock 其中一个很难测的方法(比如调用了外部 api),而其他方法逻辑很复杂你不想重写,这时用 spy 最省事。
- 场景二:验证真实调用。你想确保某个方法被调用了,同时还想验证它执行后的真实副作用。
🛠️ 怎么用?(核心语法)
在 spring boot 项目中,我们通常分两种情况使用 spy:纯单元测试 和 spring 容器集成测试。
1. 纯单元测试(使用@spy)
这是 mockito 的原生用法,用于测试普通的 java 类。
关键点:使用 spy 时,存根语法(stubbing)必须换!
- mock 用:
when(mock.method()).thenreturn(...) - spy 用:
doreturn(...).when(spy).method()- 为什么?因为 spy 默认执行真实方法,如果用
when(spy.method()),真实方法会立即执行,可能导致空指针异常。
- 为什么?因为 spy 默认执行真实方法,如果用
@extendwith(mockitoextension.class)
class userservicetest {
// 1. 必须初始化真实对象!不能写 @spy private userservice userservice; (这样会报空指针)
@spy
private userservice userservice = new userservice();
@mock
private userrepository userrepository;
@test
void testspyusage() {
// --- arrange ---
// 假设 userservice 有个方法 calculatetax() 很复杂,我们想 mock 它
// 注意语法:doreturn(...).when(spy).method()
doreturn(100.0).when(userservice).calculatetax();
// --- act ---
// 调用其他未 mock 的方法,会执行真实逻辑
// 调用 calculatetax,会返回 100.0
double tax = userservice.calculatetax();
// --- assert ---
assertequals(100.0, tax);
// --- verify ---
// 验证真实方法是否被调用
verify(userservice, times(1)).calculatetax();
}
}2. spring 集成测试(使用@spybean)
当你使用 @springboottest 时,普通的 @spy 无法替换 spring 容器里的 bean。这时要用 spring boot 提供的 @spybean。
作用:把 spring 容器里原本的 bean 替换成一个 spy 对象。
@springboottest
class orderserviceintegrationtest {
@autowired
private orderservice orderservice; // 真实的 service
// 1. @spybean:替换容器里的 userservice,但保留真实逻辑
@spybean
private userservice userservice;
@test
void testorderwithspybean() {
// --- arrange ---
// 拦截 getuserlevel 方法,返回 "vip"
doreturn("vip").when(userservice).getuserlevel(anylong());
// --- act ---
// 调用 orderservice,它会调用 userservice.getuserlevel
// 此时 getuserlevel 返回 "vip",但 userservice 的其他方法(如 saveuser)仍走真实数据库逻辑(如果配置了的话)
orderservice.createorder(1l);
// --- verify ---
// 验证 getuserlevel 确实被调用了
verify(userservice).getuserlevel(1l);
}
}⚖️ mock vs spy:怎么选?
为了让你更清晰地做决定,我整理了这个对比表:
| 维度 | @mock (完全模拟) | @spy / @spybean (部分模拟) |
|---|---|---|
| 真实代码执行 | 绝不执行 | 默认执行 (除非被拦截) |
| 初始化要求 | 不需要实例化 | 必须有真实实例 (new object()) |
| 存根语法 | when(mock.method())... | doreturn(...).when(spy)... |
| 风险 | 低(完全隔离) | 中(真实代码可能抛异常或依赖数据库) |
| 适用场景 | 依赖对象(repository, client) | 被测对象本身(想测部分逻辑)、遗留代码 |
⚖️ spy vs injectmocks
| 维度 | @spy | @injectmocks |
|---|---|---|
| 核心职责 | 部分模拟。包装一个真实对象,保留真实逻辑,但允许拦截特定方法。 | 依赖注入。创建被测对象,并自动把 @mock 或 @spy 塞进去。 |
| 代码行为 | 默认执行真实代码。 | 负责初始化对象(通过构造函数或字段注入)。 |
| 语法陷阱 | 必须手动初始化实例(= new userservice()),否则报错。 | 不需要手动初始化,mockito 会自动帮你 new 出来。 |
| 常用搭配 | 用于被测对象本身(当你不想 mock 所有方法时)。 | 用于被测对象(当你想完全隔离,只测逻辑流转时)。 |
| 存根语法 | 必须用 doreturn(...).when(spy)... | (它本身不存根,它注入的对象如果是 mock,则用 when...thenreturn) |
💡 避坑指南
- 初始化陷阱:使用 @spy 时,字段必须手动初始化(如 = new userservice()),否则 mockito 无法创建 spy 对象,会报 nullpointerexception。
- final 方法:和 mock 一样,spy 也无法 spy final 方法。调用 final 方法时,永远执行真实代码,无法拦截。
- 自调用问题:在 spring 中,如果一个 bean 的方法 a 调用了同一个类的方法 b(this.methodb()),即使你 spy 了方法 b,a 调用 b 时走的也是真实逻辑,spy 的拦截可能失效(因为 spring aop 代理机制)。
总结建议:在单元测试中,优先使用 @mock,因为它更干净、更安全。只有当你真的需要保留真实逻辑,或者为了省事不想 mock 所有依赖时,才使用 @spy。
到此这篇关于springboot结合junit单元测试的实现的文章就介绍到这了,更多相关springboot junit单元测试内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论