Day36 - Java基础加强 - 1. 单元测试 Junit


1. 单元测试 Junit


1.1 什么是单元测试?(掌握)

对部分代码进行测试。

🧠 理论理解
单元测试是对程序的最小可测试单元(如方法、函数、类)进行验证的过程,主要目的是保证代码的正确性、健壮性。它强调“最小单元”的粒度,一般是一个方法或类。单元测试的最大价值在于早发现错误降低维护成本,也是TDD(测试驱动开发)的基石。

🏢 企业实战理解
字节跳动:在服务端项目中,单元测试是 CI/CD 流水线中的“必测项”,未覆盖单元测试的代码无法合并主干。
阿里巴巴:强调“代码未测试=有缺陷”,每次提交必须附带单元测试,特别是支付、订单等核心模块,单元测试覆盖率要求≥80%。
Google:倡导“测试即开发”,GTest等工具覆盖从底层库到核心业务逻辑,单元测试+Mock组件是标配。
OpenAI:GPT API 的输入输出处理模块严格使用单元测试验证输入规范和模型响应格式,避免线上出错。

 

面试题(阿里巴巴):

你能说一下什么是单元测试吗?它和集成测试的区别是什么?为什么企业一定要写单元测试?

参考答案:
单元测试(Unit Test)是对程序中最小可测试单元的验证,通常是一个方法、一个类或者一个模块。它的目标是验证代码逻辑是否正确,能够保证局部功能没有缺陷。
单元测试通常是白盒测试,聚焦在开发者自己写的逻辑中,快速发现潜在问题。

区别:

  • 单元测试:测试最小代码单元(如一个方法);

  • 集成测试:测试多个模块或子系统之间的集成点,验证它们能否一起工作;

  • 系统测试:完整的端到端业务验证(黑盒)。

企业强制推行单元测试的原因:

  • 提前发现问题,避免小 bug 演变为大事故;

  • 降低回归成本,特别是大规模重构时可以验证是否破坏了旧功能;

  • 提高代码质量,团队可读性、可维护性强;

  • 与 CI/CD 流程结合,实现自动化测试 + 自动部署

补充(阿里常问): 单元测试是“左移质量”理念的重要体现,越早发现问题成本越低。

场景题(阿里巴巴):

你在重构一个大型遗留系统时,发现项目几乎没有单元测试,你会怎么做?请详细说说你的思路。

参考答案:
首先,我会评估系统风险,确认哪些模块是高危的(如支付、订单、用户权限模块),优先为它们补全单元测试。
步骤如下:
1️⃣ 梳理关键路径:用业务流程图或代码静态分析工具找出系统中最核心的功能点。
2️⃣ 编写“金丝雀测试”:先写少量关键路径测试用例,快速建立信心,确保重构过程不引入灾难性错误。
3️⃣ 模块化拆解:将大块的、难以测试的“上帝类”拆成小模块,每个模块单独建立测试类。
4️⃣ 引入 Mock 工具:对外部依赖(数据库、网络)使用 Mockito 等框架隔离,保证测试是纯单元级别
5️⃣ 结合 CI/CD:把单元测试纳入持续集成流水线,强制执行,确保每次提交都触发测试验证。

阿里内部有一句话:“测试是重构的保护伞”。没有单元测试的代码,重构成本非常高,甚至可能导致全局崩盘。

 


1.2 Junit 的特点?(掌握)

  • 是一个第三方的工具。(把别人写的代码导入项目中)(专业叫法:导 jar 包)

  • 如果运行结果显示绿色,表示运行结果是正确的。
    如果运行结果显示红色,表示运行结果是错误的。

🧠 理论理解
Junit 是 Java 语言最流行的单元测试框架之一,特点:

  • 绿色条/红色条视觉化反馈测试结果

  • 支持注解驱动(@Test、@Before、@After 等)

  • 提供断言机制(Assert)判断预期值与实际值是否相符

  • 易集成,支持与 Ant、Maven、Gradle 等构建工具结合

🏢 企业实战理解
美团点评:在大促项目中,测试用例全部用 Junit 自动跑,用绿色/红色标识异常,一旦失败立即触发报警。
腾讯:内网封装了自研版 Junit,结合定制化测试平台,实现接口级、单元级一键回归。
字节跳动:Junit 与持续集成系统(如 Jenkins)绑定,强制检测关键路径的回归测试结果。
OpenAI:虽主要用 Python,但 Java 侧的工具链(如接口转发器)使用 Junit+Mockito 高密度验证,避免服务异常。

 

面试题(美团):

讲讲 Junit 有哪些核心特点?它的运行机制是什么?

参考答案:
Junit 的核心特点包括:
1️⃣ 注解驱动:通过 @Test@Before@After 等注解标记测试方法及生命周期钩子。
2️⃣ 断言机制:提供丰富的 Assert 工具方法,支持各种数据类型比对,如 assertEqualsassertTrueassertNotNull 等。
3️⃣ 测试报告可视化:绿色条代表成功,红色条代表失败,支持集成到 IDE 或持续集成工具链中。
4️⃣ 可扩展性强:Junit 与 Mock 框架(如 Mockito)、Spring Test 等无缝集成。
5️⃣ 兼容性好:支持与 Maven、Gradle 等自动化构建工具集成,适配各种开发环境。

运行机制简述:

  • 加载测试类 → 扫描测试方法(识别 @Test 标记)→ 执行 @Before → 执行测试用例 → 执行 @After

  • Junit 在执行时会实例化测试类的对象,所以每个测试方法是独立执行,互不影响。

企业常问延伸: Junit 的测试方法必须是无参无返回值的实例方法,否则无法直接被识别和执行。

场景题(美团):

你们在日常开发中遇到过 Junit 的断言误用导致 bug 漏网的情况吗?请举例说明并说说怎么避免。

参考答案:
有遇到过这种情况,最典型的是:

  • assertTrue() 判断对象是否非空时,写成 assertTrue(object != null),但对象虽然不为空,内部数据其实是错误的,结果测试通过,但业务异常。

  • 另一个误区是用 assertEquals(a, b) 时,数据类型不一致(比如 Integer vs Long),导致明明内容相等但测试失败。

案例:某次我们测试订单金额,assert 时直接用浮点数比对,结果因为精度问题偶发失败,排查半天才发现应该用 BigDecimal.compareTo()

如何避免:
1️⃣ 使用专用断言方法:比如判断空对象用 assertNotNull() 而不是手写条件。
2️⃣ 注意数据类型精度:特别是涉及金额、浮点数的断言时,封装工具类做二次比对。
3️⃣ 二次复核:Code Review 时专门检查断言写法,避免逻辑陷阱。

美团内部有“断言模板”制度,常用场景都会统一规范,减少误用风险。

 


1.3 基本用法:(掌握)

1️⃣ 一定要先写一个方法。
2️⃣ 在这个方法的上面写 @Test
3️⃣ 鼠标点一下 @Test,按 Alt + 回车,点击 Junit4

此时就可以自动导包。
如果自动导包失败(连接外网,或者自己手动导包)
如果导包成功在左下角就会出现 Junit4 的相关 jar 包

🧠 理论理解
Junit 的用法核心在于注解驱动 + 自动断言

  • @Test 标记方法为测试用例

  • 自动识别 无参无返回值的非静态方法

  • 运行时通过绿色(成功)或红色(失败)直观展示测试结果
    导包可以通过自动导入,也可以手动导入 jar 包,保证离线环境下正常工作。

🏢 企业实战理解
阿里巴巴:高并发类库的单元测试中,为了避免误用静态方法做测试,团队有严格的代码审查流程,确保每个 @Test 方法都是实例方法
京东:在历史遗留系统中常用手动导包(lib 目录方式)集成 Junit,避免因网络依赖导致构建失败。
Google:GCP(Google Cloud Platform)SDK 的 Java 客户端工具,也使用标准 Junit 测试工具链,跑用例时支持远程 CI 节点拉取 jar 包自动化集成。

 


手动导包(掌握)

1️⃣ 在当前模块下,右键新建一个文件夹(lib)
2️⃣ 把资料里的两个 jar 包拷贝到 lib 文件夹里面
3️⃣ 选中两个 jar 右键点击 add as a lib...
4️⃣ 到代码中找到 @Test,按 Alt + 回车 再导入


运行测试代码(掌握)

  • 只能直接运行无参无返回值非静态方法

  • 想要运行谁,就右键点击哪个方法。如果想要运行一个类里面所有的测试方法,选择类名,右键点击即可


Junit 正确的打开方式(掌握)

注意点:并不是直接在要测试的方法上面直接加 @Test

原因:要测试的方法有可能是有参数的、有返回值的,或者是静态的。


正确的使用方式(掌握)

1️⃣ 新建测试类
2️⃣ 新建测试方法(一般用要测试的方法名 + Test)
3️⃣ 在这个方法中直接调用要测试的方法
4️⃣ 在测试方法的上面写 @Test

代码示例:

// 测试用例(测试类)
public class JunitTest {

    @Test
    public void method2Test() {
        // 调用要测试的方法
        JunitDemo1 jd = new JunitDemo1();
        jd.method2(10);
    }
}

面试题(字节跳动):

Junit 为什么要求测试方法是无参、无返回值的?如果我的方法是有参/有返回值,怎么测试?

参考答案:
Junit 的测试方法设计为无参无返回值是为了简化调用流程,它通过反射机制自动调用测试方法,不需要人为传入参数或处理返回值,保证用例能够被批量自动识别和执行。

如果被测方法是有参数/有返回值的:
✅ 我们在测试方法中手动创建测试数据调用实际方法,将结果与预期值用 Assert 工具比对。
✅ 示例:

@Test
public void addTest() {
    int result = Calculator.add(2, 3);
    Assert.assertEquals(5, result);
}

这种方式解决了 Junit 无法直接处理有参方法的限制。

字节延伸问题:

实际项目里你们有没有遇到过 Junit 自动导包失败的情况?怎么处理?
✅ 答:有,特别是国内离线环境时,IDE 无法联网拉取依赖,这时候采用手动导包的方式(lib 目录 + add as library)就可以解决问题。

 

场景题(字节跳动):

假设你现在需要测试一个工具类,它的方法是静态方法且有多个参数,你会怎么设计单元测试?需要注意什么?

参考答案:
工具类的静态方法无法直接加 @Test 注解测试(因为它不是测试方法本身,而是被调用的方法)。
设计步骤:
1️⃣ 新建一个单元测试类(比如 MyUtilsTest)。
2️⃣ 每个测试方法对应一个功能点,命名清晰(如 calculateSumTest())。
3️⃣ 在测试方法中直接调用工具类的静态方法,用 Assert 检查结果是否符合预期。
示例:

@Test
public void calculateSumTest() {
    int result = MyUtils.calculateSum(2, 3, 5);
    Assert.assertEquals(10, result);
}

注意点:

  • 参数覆盖要全面(正常值、边界值、异常值)

  • 断言清晰,提示语详细(出现异常时能快速定位)

  • 如果方法有副作用(如写文件),要在测试前后做清理操作

字节内部有个约定:即使是工具类也必须有单元测试,不能因为它简单就跳过。


实际开发中单元测试的使用方式(掌握)

需求:测试 File 中的 delete 方法是否书写正确

开发中的测试原则:
不污染原数据

代码示例:

public class JunitDemo3 {

    @Before
    public void beforemethod() throws IOException {
        // 备份文件
        File src = new File("C:\\Users\\moon\\Desktop\\a.txt");
        File dest = new File("C:\\Users\\moon\\Desktop\\copy.txt");

        FileInputStream fis = new FileInputStream(src);
        FileOutputStream fos = new FileOutputStream(dest);
        int b;
        while ((b = fis.read()) != -1) {
            fos.write(b);
        }
        fos.close();
        fis.close();
    }

    @Test
    public void method() {
        File file = new File("C:\\Users\\moon\\Desktop\\a.txt");
        boolean delete = file.delete();

        boolean exists = file.exists();

        Assert.assertEquals("delete方法出错了", delete, true);
        Assert.assertEquals("delete方法出错了", exists, false);
    }

    @After
    public void aftermethod() throws IOException {
        // 还原文件
        File src = new File("C:\\Users\\moon\\Desktop\\copy.txt");
        File dest = new File("C:\\Users\\moon\\Desktop\\a.txt");

        FileInputStream fis = new FileInputStream(src);
        FileOutputStream fos = new FileOutputStream(dest);
        int b;
        while ((b = fis.read()) != -1) {
            fos.write(b);
        }
        fos.close();
        fis.close();

        // 删除备份文件
        src.delete();
    }
}

🧠 理论理解
单元测试的“标准用法”不仅仅是加 @Test 测一下,还应具备完整的三阶段结构
1️⃣ @Before → 测试前准备(数据/环境搭建)
2️⃣ @Test → 测试执行
3️⃣ @After → 测试后清理(恢复数据/释放资源)
这种结构保证测试过程可控、可复现、不污染原始数据,是企业级代码质量的保证。

🏢 企业实战理解
美团外卖:测试数据库操作时,测试代码都会备份一份原始数据,单元测试完毕自动回滚,确保不影响真实业务数据。
字节跳动飞书:对文件系统类 API 的测试,同样用 @Before/@After 机制在测试前后创建和清理临时文件,保证云盘不会残留垃圾数据。
OpenAI:在测试 GPT 接口的限流逻辑时,使用 @Before 初始化 API Key 环境,@After 销毁测试账户,防止滥用。

 

面试题(腾讯):

@Before 和 @After 注解在单元测试中起到什么作用?请举个例子说明你是如何在实际项目中使用它们的。

参考答案:
@Before@After测试生命周期注解

  • @Before 用于在每个测试方法执行之前做准备工作,如初始化数据、创建资源等;

  • @After 用于在测试执行后清理环境,如删除测试数据、关闭连接等。

它们保证每个测试方法执行前后都是干净的环境,防止用例间互相影响。

✅ 示例(实际案例):
在文件操作单元测试中,

  • @Before 先复制一份备份文件,

  • @Test 进行删除文件测试,

  • @After 再将备份文件恢复回来,保证每次运行完测试都能恢复原样

腾讯经验补充: 企业中常用这种“前置-后置钩子”机制防止污染数据,特别是在支付、订单等场景,如果测试数据被污染会直接影响生产环境稳定性。

场景题(腾讯):

你在开发文件上传功能,单元测试中涉及真实文件创建、删除,这会不会有潜在风险?你是如何保证测试的安全性和稳定性的?

参考答案:
这种测试场景确实存在潜在风险,比如:

  • 误删除生产文件

  • 残留测试垃圾文件,占用磁盘空间

  • 权限问题导致测试失败

我的做法:
1️⃣ 引入隔离目录:所有测试用的文件只在 test_resources 目录下创建,不允许访问生产目录。
2️⃣ 在 @Before 中准备数据,比如复制一份临时文件;
3️⃣ 在 @After 中确保100%清理,哪怕测试中途抛异常,也能走到后置清理逻辑;
4️⃣ 加路径断言,强制校验测试运行路径必须包含“/test/”关键词,如果误指向其他路径立刻 fail;
5️⃣ CI 环境专门挂载临时磁盘,避免污染宿主机文件系统。

腾讯经验:我们测试文件操作时,都会引入“沙箱环境”概念,把所有临时数据包在专门的测试沙箱里,执行完毕立即销毁,保证无风险。

 


作业

测试 Properties 类中的 store 方法是否书写正确

🧠 理论理解
Properties 是 Java 内置的配置文件存储类,其 store 方法作用是将内存中的键值对序列化保存到本地文件。单元测试关注的点:

  • 文件是否成功生成

  • 文件内容是否完整且与预期一致

  • 文件是否能被再次正确读取

通常结合断言验证文件是否存在、是否大于 0 字节,以及重新加载的内容是否匹配原数据。

🏢 企业实战理解
阿里巴巴:大量使用 Properties 存储配置参数,Junit 测试时会创建临时文件夹写入数据,并用断言保证多语言配置存储正常。
京东:在自动化测试流程中,Properties 文件存储的支付配置项是敏感数据,测试时用加密 mock 文件进行覆盖测试。
字节跳动:用 Properties 文件测试国际化资源包,结合断言保证多语言 key-value 写入无误,防止因乱码导致 UI 崩溃。

 

面试题(美团):

你测试 Properties 的 store 方法时,会设计哪些断言?如何保证测试结果的有效性?

参考答案:
我会设计以下几个关键断言确保 store 方法正常:
1️⃣ 文件存在性检查:断言 store() 方法执行后,目标文件是否真正生成。
2️⃣ 文件大小检查:文件大小应大于 0,证明有数据写入;
3️⃣ 数据一致性检查:再次用 Properties.load() 重新加载文件,验证读到的数据和内存中的数据完全一致。

确保结果有效性的做法:

  • 用 @Before 提前初始化 Properties 对象并准备好测试数据

  • 用 @After 删除测试生成的文件,保证环境干净,避免影响其他测试。

  • 断言失败时输出详细提示,便于快速定位错误位置。

美团延伸问法:

如果存储的配置中包含中文,store 方法是否有坑?
✅ 答:store 方法默认以 ISO-8859-1 编码保存中文会转 Unicode 编码(如 \u4e2d\u6587),所以读取时需要用 load() 方法自动转码,或明确标注编码(JDK 8 之后支持 store(Writer, ...))。

场景题(阿里巴巴):

你们测试 Properties.store() 方法时发现:线上偶尔出现文件乱码,但测试用例始终通过。你会怎么排查这个问题并改进测试?

参考答案:
这个问题很典型,根因通常是编码不一致

  • store 方法默认以 ISO-8859-1 编码保存,如果写入时是 UTF-8,读取时忘记转码就会乱码。

  • 本地测试环境(Windows/IDEA)和线上环境(Linux)编码环境不一致,导致“本地测试没问题,线上就挂”。

我的排查步骤:
1️⃣ 检查测试代码:是否强制指定了 store() 方法的编码格式?
2️⃣ 检查 CI 环境变量:系统默认编码是不是 UTF-8?
3️⃣ 增加断言:不仅检查 key-value 内容,还断言文件物理内容是否包含 Unicode 转义符。
4️⃣ 引入多环境测试:在 CI 流水线中加上 Linux 容器模拟生产环境执行用例,防止平台差异。
5️⃣ 推荐改用 store(Writer, comment) 方案,支持自定义编码(UTF-8)。

阿里内部经验:涉及配置文件的单元测试,必须“跨平台测试 + 编码断言”双保险,防止隐形 bug。

 


开发心得

1️⃣ @Before → 准备数据
2️⃣ @Test → 测试方法
3️⃣ @After → 还原


Before

// 准备数据
// 1. 创建 Properties 对象
// 2. put 数据到集合当中
// (注意:Properties 对象可以放到成员位置,方便在多个方法中使用)

Test

// 调用 store 方法,保存数据到本地文件

// 断言1:判断当前文件是否存在
// 断言2:文件大小 > 0
// 断言3:再次读取文件中的数据,判断和集合中数据是否一致

// 如果所有断言都通过,表示 store 方法正确

After

// 删除本地文件

扩展点

在单元测试中,相对路径是相对当前模块而言的。

代码示例:

File file = new File("aweihaoshuai.txt");
file.createNewFile();
// 会把 aweihaoshuai.txt 文件新建到模块目录中

扩展点

🧠 理论理解
在单元测试中,相对路径是相对于当前模块根路径的(即 IDEA/项目的 module 目录)。这意味着,创建文件时如果不指定绝对路径,文件默认存储在模块根目录,方便快速定位测试用例生成的文件。

🏢 企业实战理解
腾讯云:单元测试生成的临时文件(如日志、临时缓存)统一放在模块根目录,避免散落导致 CI/CD 流水线清理失败。
美团:测试时自动创建模块级 temp 目录,所有测试输出文件集中存储,保证 CI 环境一致性。
OpenAI:内部工具链规定测试文件必须使用相对路径写入项目根路径,便于在不同机器环境运行一致。

 

面试题(OpenAI):

单元测试中你用相对路径写文件,有什么风险?在 CI/CD 流水线中怎么避免路径问题?

参考答案:
相对路径虽然方便,但风险在于:
1️⃣ 路径依赖上下文:不同机器、不同工具链跑测试时,项目的工作目录可能不一样;
2️⃣ CI/CD 环境:流水线往往有自定义的工作路径,如果测试用例中 hardcode 了相对路径,可能导致找不到文件或写入错误位置。

解决方案:

  • 推荐在项目模块根目录创建专门的 temp 目录存放测试输出文件,并在测试类中通过 new File(System.getProperty("user.dir")) 获取绝对路径拼接,确保路径准确。

  • ✅ 在 CI 中配置路径参数化(如 Jenkins 的 workspace 环境变量),让路径动态适配执行环境。

OpenAI 实际经验补充: 测试用例执行完毕后清理临时文件是硬性要求,防止磁盘空间被占满。

场景题(OpenAI):

你的单元测试中用相对路径创建文件,但上线后发现 CI/CD 流水线执行测试失败,报文件路径找不到,你怎么解决这个问题?

参考答案:
我会先确认路径问题的来源:

  • CI/CD 环境中的工作目录可能不是项目根目录,而是一个特定的挂载路径;

  • 单元测试的相对路径是基于 JVM 启动的工作目录解析的,这就可能导致路径偏移。

解决方案:
1️⃣ 用 System.getProperty("user.dir") 打印当前运行路径,确认偏移点;
2️⃣ 将测试中用到的文件路径改成基于项目根路径的绝对路径拼接,而不是纯相对路径;
3️⃣ 在 CI 流水线配置中,显式设置工作目录,确保路径一致性;
4️⃣ 如果是多模块项目,把测试文件放到 src/test/resources,并通过 ClassLoader.getResource() 加载,避免路径问题。

OpenAI 内部最佳实践:测试中涉及的文件路径都参数化,通过环境变量注入路径值,保证测试在任何机器上都能跑通。

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值