当前位置: 首页 > news >正文

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 规则稍复杂混合场景

最佳实践

  1. 单元测试:优先使用 @MockBean,避免依赖数据库、MQ 等外部资源。
  2. 部分方法需要真实执行:用 @SpyBean
  3. 集成测试 / 验证真实业务链路:用 @Resource@Autowired

http://www.dtcms.com/a/464779.html

相关文章:

  • SaaS多租户数据隔离实战:MyBatis拦截器实现行级安全方案
  • 【深入理解计算机网络08】网络层之IPv4
  • 网站的标签wordpress 导航栏居中
  • 解决电脑提示“0xc000007b错误”的简单指南
  • 【STM32项目开源】基于STM32的智能家居安防系统
  • 网络营销方式思维导图aso优化榜单
  • HKM9000视频处理卡
  • 机器视觉的平板显示屏加强膜贴合应用
  • 安卓玩机工具----手机秒变电脑麦克风
  • Win10系统笔记本电脑设置合上盖子自动锁屏
  • LLaMA: Open and Efficient Foundation Language Models 论文阅读
  • LeetCode——Hot 100【全排列】
  • 云南大理拍婚纱照价格表建网站优化
  • 双目测距实战1-环境配置
  • 2025人工智能在无人机数据处理中的应用
  • 阿里开源Qwen3-Omni-30B-A3B三剑客——Instruct、Thinking 和 Captioner
  • 长春建站程序湖南网络科技有限公司
  • xtuoj Can you raed it croretcly?
  • 异构动作空间
  • 【Nginx开荒攻略】Nginx虚拟主机配置:从域名、端口到IP的完整指南
  • 小杰深度学习(nine)——CUDA与CuDNN安装
  • 鸿蒙NEXT USB Host模式开发完全指南
  • MinerU2.5 windows 本地部署
  • UIkit中使用新版UICollectionViewCompositionalLayout进行复杂布局(二)
  • 网站建设的技术问题苏州吴江建设局招标网站
  • 河南省村镇建设处网站网站配色与布局 教材
  • Prometheus运维之路(ES监控接入)
  • OpenAMP专题(一):一文了解OpenAMP全貌
  • C++ 中 rfind 方法详解
  • SpringBoot 教程(十四) SpringBoot之集成 Redis(优化版)