目录
- 一、为什么需要单元测试?
- [二、单元测试覆盖率(JUnit Coverage)](#二、单元测试覆盖率(JUnit Coverage))
- [三、JUnit 测试生命周期注解详解与案例](#三、JUnit 测试生命周期注解详解与案例)
- [四、JUnit + 反射浅谈自动化测试框架设计](#四、JUnit + 反射浅谈自动化测试框架设计)
- [五、Mock 技术(模拟依赖)](#五、Mock 技术(模拟依赖))
- 
- 1、示例业务类
- 2、测试类框架
- 3、三种注入方式对比
- 
- 1.@Resource(真实调用)
- [2.@MockBean(完全 Mock)](#2.@MockBean(完全 Mock))
- [3.@SpyBean(部分 Mock)](#3.@SpyBean(部分 Mock))
 
 
- 六、总结对比
阿里巴巴 Java 开发手册中的单元测试要求总结
在日常开发中,单元测试 不仅仅是保证代码正确性的手段,更是代码质量与可维护性的重要保障。阿里巴巴 Java 开发手册对单元测试有明确要求,本文结合实践经验,对其要点进行总结。
结论很明确:
- 不要只看绿色条 ,关键是 assert 语句是否合理。
- 好的单元测试必须有明确断言,确保结果符合预期,而不仅仅是"代码跑通了"。
一、为什么需要单元测试?
单元测试的核心价值在于:
- 提前发现问题:在开发阶段快速发现潜在 bug,避免上线后代价更大的修复。
- 保证重构安全:修改或优化代码时,通过已有单元测试验证逻辑未被破坏。
- 提升代码质量:测试驱动开发(TDD)推动开发者从调用方角度思考接口设计。
- 降低维护成本:新同事接手项目时,单元测试就是最好的"活文档"。
入门案例
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcDemoTest
{
    /**
     * 演示正确
     */
    @Test
    void add()
    {
        CalcDemo calcDemo = new CalcDemo();
        assertEquals(4,calcDemo.add(2,2));
    }
    /**
     * 演示错误
     */
    @Test
    void 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
{
    @Test
    void scoreLevel()
    {
        ScoreDemo scoreDemo = new ScoreDemo();
        assertEquals("弱",scoreDemo.scoreLevel(52));
    }
    @Test
    void scoreLevelv2()
    {
        ScoreDemo scoreDemo = new ScoreDemo();
        assertEquals("差",scoreDemo.scoreLevel(62));
    }
    @Test
    void scoreLevelv3()
    {
        ScoreDemo scoreDemo = new ScoreDemo();
        assertEquals("中",scoreDemo.scoreLevel(80));
    }
    @Test
    void scoreLevelv4()
    {
        ScoreDemo scoreDemo = new ScoreDemo();
        assertThrows(IllegalArgumentException.class,() -> scoreDemo.scoreLevel(-7));
    }
}测试案例+带着覆盖率报告


测试案例+带着覆盖率报告多次运行

三、JUnit 测试生命周期注解详解与案例
1. 生命周期注解介绍
- 
@BeforeAll 在所有测试方法执行之前运行一次,通常用于全局资源的初始化。 👉 例如:启动数据库连接池、加载配置文件。 
- 
@BeforeEach 在每个测试方法执行前运行,常用于准备测试环境。 👉 例如:创建对象、准备测试数据。 
- 
@Test 定义具体的测试方法。 
- 
@AfterEach 在每个测试方法执行后运行,用于清理测试环境。 👉 例如:关闭文件、清空缓存。 
- 
@AfterAll 在所有测试方法执行完毕后运行一次,用于全局资源释放。 👉 例如:关闭数据库连接池。 
2、案例演示一
@BeforeEach和@AfterEach
假设我们有一个 CalcDemo 类,新增了一个减法方法:
            
            
              java
              
              
            
          
          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;
    @BeforeAll
    static void m1()
    {
        stringBuffer = new StringBuffer("abc");
        System.out.println("===============: "+stringBuffer.length());
    }
    @AfterAll
    static void m2()
    {
        System.out.println("===============: "+stringBuffer.append(" ,end").toString());
    }
    @BeforeEach
    void setUp()
    {
        System.out.println("----come in BeforeEach");
        calcDemo = new CalcDemo();
    }
    @AfterEach
    void tearDown()
    {
        System.out.println("----come in AfterEach");
        calcDemo = null;
    }
    @Test
    void add()
    {
        Assertions.assertEquals(5,calcDemo.add(1,4));
        assertEquals(5,calcDemo.add(2,3));
    }
    @Test
    void 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;
    }
    @DonglinTest
    public int div(int x ,int y)
    {
        return x / y;
    }
    @DonglinTest
    public 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、示例业务类
            
            
              java
              
              
            
          
          @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、测试类框架
            
            
              java
              
              
            
          
          @SpringBootTest
class MemberServiceTest {
    // 测试用例放这里
}3、三种注入方式对比
1.@Resource(真实调用)
            
            
              java
              
              
            
          
          @Resource
private MemberService memberService1;
@Test
void m1() {
    String result = memberService1.add(2);
    assertEquals("ok", result);
    System.out.println("----m1 over");
}✅ 特点:
- 注入 真实 Bean,方法逻辑会真正执行。
- 需要数据库/外部依赖时,也会触发真实操作。
📌 场景:适合需要真实运行逻辑的集成测试。
2.@MockBean(完全 Mock)
            
            
              java
              
              
            
          
          @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)
            
            
              java
              
              
            
          
          @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。