Java 单元测试全攻略:JUnit 生命周期、覆盖率提升、自动化框架与 Mock 技术
目录
- 一、为什么需要单元测试?
- 二、单元测试覆盖率(JUnit Coverage)
- 三、JUnit 测试生命周期注解详解与案例
- 1. 生命周期注解介绍
- 2、案例演示一
- 3、案例演示二
- 4. 小结
- 四、JUnit + 反射浅谈自动化测试框架设计
- 五、Mock 技术(模拟依赖)
- 1、示例业务类
- 2、测试类框架
- 3、三种注入方式对比
- 1.@Resource(真实调用)
- 2.@MockBean(完全 Mock)
- 3.@SpyBean(部分 Mock)
- 六、总结对比
阿里巴巴 Java 开发手册中的单元测试要求总结
在日常开发中,单元测试不仅仅是保证代码正确性的手段,更是代码质量与可维护性的重要保障。阿里巴巴 Java 开发手册对单元测试有明确要求,本文结合实践经验,对其要点进行总结。
结论很明确:
- 不要只看绿色条,关键是 assert 语句是否合理。
- 好的单元测试必须有明确断言,确保结果符合预期,而不仅仅是“代码跑通了”。
一、为什么需要单元测试?
单元测试的核心价值在于:
- 提前发现问题:在开发阶段快速发现潜在 bug,避免上线后代价更大的修复。
- 保证重构安全:修改或优化代码时,通过已有单元测试验证逻辑未被破坏。
- 提升代码质量:测试驱动开发(TDD)推动开发者从调用方角度思考接口设计。
- 降低维护成本:新同事接手项目时,单元测试就是最好的“活文档”。
入门案例
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;class CalcDemoTest
{/*** 演示正确*/@Testvoid add(){CalcDemo calcDemo = new CalcDemo();assertEquals(4,calcDemo.add(2,2));}/*** 演示错误*/@Testvoid addv2(){CalcDemo calcDemo = new CalcDemo();assertEquals(41,calcDemo.add(2,2));}
}
二、单元测试覆盖率(JUnit Coverage)
实际代码
public class ScoreDemo
{public String scoreLevel(int score){if(score <= 0) {throw new IllegalArgumentException("缺考");} else if (score < 60) {return "弱";} else if (score < 70) {return "差";} else if (score < 80) {return "中";} else if (score < 90) {return "良";} else {return "优";}}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;class ScoreDemoTest
{@Testvoid scoreLevel(){ScoreDemo scoreDemo = new ScoreDemo();assertEquals("弱",scoreDemo.scoreLevel(52));}@Testvoid scoreLevelv2(){ScoreDemo scoreDemo = new ScoreDemo();assertEquals("差",scoreDemo.scoreLevel(62));}@Testvoid scoreLevelv3(){ScoreDemo scoreDemo = new ScoreDemo();assertEquals("中",scoreDemo.scoreLevel(80));}@Testvoid scoreLevelv4(){ScoreDemo scoreDemo = new ScoreDemo();assertThrows(IllegalArgumentException.class,() -> scoreDemo.scoreLevel(-7));}
}
测试案例+带着覆盖率报告
测试案例+带着覆盖率报告多次运行
三、JUnit 测试生命周期注解详解与案例
1. 生命周期注解介绍
-
@BeforeAll
在所有测试方法执行之前运行一次,通常用于全局资源的初始化。
👉 例如:启动数据库连接池、加载配置文件。 -
@BeforeEach
在每个测试方法执行前运行,常用于准备测试环境。
👉 例如:创建对象、准备测试数据。 -
@Test
定义具体的测试方法。 -
@AfterEach
在每个测试方法执行后运行,用于清理测试环境。
👉 例如:关闭文件、清空缓存。 -
@AfterAll
在所有测试方法执行完毕后运行一次,用于全局资源释放。
👉 例如:关闭数据库连接池。
2、案例演示一
@BeforeEach和@AfterEach
假设我们有一个 CalcDemo
类,新增了一个减法方法:
public class CalcDemo
{public int add(int x ,int y){return x + y;//return x * y;}public int sub(int x ,int y){return x - y;}
}
生成测试工具类
有重复代码
解决方案
结论
3、案例演示二
@BeforeAll和@AfterAll
测试代码
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;class CalcDemoTestV2
{CalcDemo calcDemo = null;static StringBuffer stringBuffer = null;@BeforeAllstatic void m1(){stringBuffer = new StringBuffer("abc");System.out.println("===============: "+stringBuffer.length());}@AfterAllstatic void m2(){System.out.println("===============: "+stringBuffer.append(" ,end").toString());}@BeforeEachvoid setUp(){System.out.println("----come in BeforeEach");calcDemo = new CalcDemo();}@AfterEachvoid tearDown(){System.out.println("----come in AfterEach");calcDemo = null;}@Testvoid add(){Assertions.assertEquals(5,calcDemo.add(1,4));assertEquals(5,calcDemo.add(2,3));}@Testvoid sub(){assertEquals(5,calcDemo.sub(10,5));}
}
结论
4. 小结
- @BeforeEach
void setUp() 每一个测试方法调用前必执行的方法 - @AfterEach
void tearDown() 每一个测试方法调用后必执行的方法 - @BeforeAll
所有测试方法调用前执行一次,在测试类没有实例化之前就已被加载,需用static修饰 - @AfterAll
所有测试方法调用后执行一次,在测试类没有实例化之前就已被加载,需用static修饰
特性 | @BeforeEach | @BeforeAll |
---|---|---|
执行次数 | 每个测试方法前执行一次 | 所有测试方法前只执行一次 |
方法修饰符 | 普通方法即可 | 必须是 static 方法(JUnit 5 中可配合 @TestInstance(Lifecycle.PER_CLASS) 去掉 static) |
适用场景 | 每个测试都需要独立、干净的测试环境 | 所有测试共享同一个全局资源 |
性能表现 | 多次执行,性能相对较低 | 只执行一次,性能更高 |
典型用法 | 初始化临时对象、准备测试数据 | 启动数据库连接池、初始化 Spring 上下文 |
四、JUnit + 反射浅谈自动化测试框架设计
需求说明
public class CalcHelpDemo
{public int mul(int x ,int y){return x * y;}@DonglinTestpublic int div(int x ,int y){return x / y;}@DonglinTestpublic void thank(int x ,int y){System.out.println("3ks,help me test bug");}
}
自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DonglinTest {
}
编写业务类
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import com.donglin.interview2.junit.DonglinTest;
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;/*** Junit+反射+注解浅谈自动测试框架设计** 需求* 1 我们自定义注解@DonglinTest* 2 将注解DonglinTest加入需要测试的方法* 3 类AutoTestClient通过反射检查,哪些方法头上标注了DonglinTest注解会自动进行单元测试*/
@Slf4j
public class AutoTestClient
{public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException{//家庭作业,抽取一个方法,(class,p....)CalcHelpDemo calcHelpDemo = new CalcHelpDemo();int para1 = 10;int para2 = 0;Method[] methods = calcHelpDemo.getClass().getMethods();AtomicInteger bugCount = new AtomicInteger();// 要写入的文件路径(如果文件不存在,会创建该文件)String filePath = "BugReport"+ (DateUtil.format(new Date(), "yyyyMMddHHmmss"))+".txt";for (int i = 0; i < methods.length; i++){if (methods[i].isAnnotationPresent(DonglinTest.class)){try{methods[i].invoke(calcHelpDemo,para1,para2);//放行} catch (Exception e) {bugCount.getAndIncrement();log.info("异常名称:{},异常原因:{}",e.getCause().getClass().getSimpleName(),e.getCause().getMessage());FileUtil.writeString(methods[i].getName()+"\t"+"出现了异常"+"\n", filePath, "UTF-8");FileUtil.appendString("异常名称:"+e.getCause().getClass().getSimpleName()+"\n", filePath, "UTF-8");FileUtil.appendString("异常原因:"+e.getCause().getMessage()+"\n", filePath, "UTF-8");}finally {FileUtil.appendString("异常数:"+bugCount.get()+"\n", filePath, "UTF-8");}}}}
}/*** 在Hutool工具包中,使用FileUtil类进行文件操作时,通常不需要显式地“关闭”文件。* 这是因为Hutool在内部处理文件I/O时,已经考虑了资源的自动管理和释放。** 具体来说,当你使用FileUtil的静态方法(如writeString、appendString、readUtf8String等)时,* 这些方法会在执行完毕后自动关闭与文件相关的流和资源。因此,你不需要(也不能)* 调用类似于close这样的方法来关闭文件。** 这是因为这些静态方法通常使用Java的try-with-resources语句或其他类似的机制来确保资源在* 不再需要时得到释放。try-with-resources是Java 7及更高版本中引入的一个特性,* 它允许你在try语句块结束时自动关闭实现了AutoCloseable或Closeable接口的资源。** 所以,当你使用Hutool的FileUtil类进行文件操作时,你可以放心地编写代码,* 而无需担心资源泄露或忘记关闭文件的问题。Hutool已经为你处理了这些细节。*/
五、Mock 技术(模拟依赖)
在日常 Spring Boot 开发中,我们经常需要对业务逻辑进行单元测试。
然而,测试时到底该用 真实 Bean,还是 Mock 对象,甚至是 部分 Mock,往往让人困惑。
本文结合一个简单的 MemberService
示例,带你梳理 @Resource、@MockBean、@SpyBean 在单元测试中的区别与应用场景。
1、示例业务类
@Service
public class MemberService {public String add(Integer uid) {System.out.println("---come in addUser");if (uid == -1) throw new IllegalArgumentException("parameter is negative。。。。");return "ok";}public int del(Integer uid) {System.out.println("---come in del");return uid;}
}
这里我们有两个方法:
add()
:新增用户,参数非法时抛异常。del()
:删除用户,简单返回 uid。
2、测试类框架
@SpringBootTest
class MemberServiceTest {// 测试用例放这里
}
3、三种注入方式对比
1.@Resource(真实调用)
@Resource
private MemberService memberService1;@Test
void m1() {String result = memberService1.add(2);assertEquals("ok", result);System.out.println("----m1 over");
}
✅ 特点:
- 注入 真实 Bean,方法逻辑会真正执行。
- 需要数据库/外部依赖时,也会触发真实操作。
📌 场景:适合需要真实运行逻辑的集成测试。
2.@MockBean(完全 Mock)
@MockBean
private MemberService memberService2;@Test
void m2_NotMockRule() {String result = memberService2.add(2);assertEquals("ok", result); // ❌ 这里会报错,因为默认返回 null
}@Test
void m2_WithMockRule() {when(memberService2.add(3)).thenReturn("ok");String result = memberService2.add(3);assertEquals("ok", result);System.out.println("----m2_WithMockRule over");
}
✅ 特点:
- 注入的 Bean 被 完全替换成 Mock 对象。
- 没有指定规则时,返回默认值(对象 null、数字 0、布尔 false)。
- 指定规则后,返回指定结果,不会执行真实逻辑。
📌 场景:适合需要 完全隔离外部依赖 的单元测试。
3.@SpyBean(部分 Mock)
@SpyBean
private MemberService memberService3;@Test
void m3() {when(memberService3.add(2)).thenReturn("ok");// add() 按规则走String result = memberService3.add(2);assertEquals("ok", result);// del() 没有规则,走真实逻辑int result2 = memberService3.del(3);assertEquals(3, result2);System.out.println("----over");
}
✅ 特点:
- 注入的 Bean 是 真实 Bean 的代理。
- 如果方法设置了 Mock 规则,就走规则;否则走真实逻辑。
📌 场景:适合测试中既想 Mock 某些方法,又想保留其他方法的真实逻辑。
六、总结对比
注解 | 行为 | 优点 | 缺点 | 使用场景 |
---|---|---|---|---|
@Resource / @Autowired | 注入真实 Bean | 方法真实执行,覆盖面完整 | 依赖数据库、外部服务,测试环境复杂 | 集成测试 |
@MockBean | 完全 Mock | 彻底隔离外部依赖,运行快 | 无规则时返回默认值,逻辑不执行 | 纯单元测试 |
@SpyBean | 部分 Mock | 既能 Mock 指定方法,又能执行真实逻辑 | 管理 Mock 规则稍复杂 | 混合场景 |
最佳实践
- 单元测试:优先使用
@MockBean
,避免依赖数据库、MQ 等外部资源。 - 部分方法需要真实执行:用
@SpyBean
。 - 集成测试 / 验证真实业务链路:用
@Resource
或@Autowired
。