JUnit单元测试学习笔记
一、这一章在讲什么
这一章主要学习 JUnit 单元测试。单元测试就是针对一个最小功能单元,通常是一个方法,编写测试代码来验证它是否按预期工作。以前可以用 main 方法手动测试,但这种方式测试代码和业务代码混在一起,不方便维护,也不利于自动化。JUnit 可以把测试代码和源代码分开,并通过断言自动判断测试是否成功。结合 CalculatorTest.java 来看,重点是理解 @Test、断言、异常测试、批量断言、测试目录规范和 Maven 中的 scope=test。
二、核心概念
1. 单元测试
-
它是什么
- 单元测试就是针对最小的功能单元进行测试。
- 在 Java 中,最小功能单元通常就是一个方法。
-
有什么作用
- 检查方法的执行结果是否符合预期;
- 提前发现代码错误;
- 后期修改代码时,可以快速验证有没有把旧功能改坏。
-
它的原理
- 写一个专门的测试方法;
- 在测试方法中调用被测试的代码;
- 用断言判断实际结果和预期结果是否一致。
-
初学者容易混淆的点
- 单元测试不是业务代码;
- 单元测试不是简单打印结果;
System.out.println()只能让人看,断言才能让测试框架自动判断对错。
2. JUnit
- 它是什么
- JUnit 是 Java 中常用的单元测试框架。
- 你的
CalculatorTest.java用的是 JUnit 5,因为导入的是:
java
import org.junit.jupiter.api.Test;
-
有什么作用
- 让测试方法不需要
main方法也能运行; - 可以自动执行多个测试方法;
- 可以自动判断测试成功或失败;
- 可以生成测试结果,成功通常显示绿色,失败通常显示红色。
- 让测试方法不需要
-
它的原理
- JUnit 测试引擎会扫描测试类;
- 找到带有
@Test等测试注解的方法; - 自动调用这些方法;
- 根据断言结果判断测试是否通过。
-
初学者容易混淆的点
- 不是方法名叫
test就一定会执行; - 关键是方法上要有
@Test注解; - JUnit 执行测试方法,不依赖自己写
main方法。
- 不是方法名叫
3. @Test 注解
-
它是什么
@Test是 JUnit 中用来标记测试方法的注解。
-
有什么作用
- 告诉 JUnit:这个方法是测试方法,可以被自动执行。
-
它的原理
- JUnit 运行时会识别带有
@Test的方法; - 然后通过测试引擎调用这些方法。
- JUnit 运行时会识别带有
-
初学者容易混淆的点
- 去掉
@Test后,这个方法就只是普通方法; - 普通方法不会被 JUnit 自动当成测试执行。
- 去掉
4. 断言
-
它是什么
- 断言就是用代码写出"我期望的结果",让 JUnit 判断实际结果是否符合预期。
-
有什么作用
- 自动判断测试成功或失败;
- 替代人工看控制台输出;
- 让测试结果更明确。
-
它的原理
- 如果断言条件满足,测试继续执行;
- 如果断言条件不满足,测试失败,JUnit 会报告错误信息。
-
初学者容易混淆的点
- 断言失败时,不是"执行第三个参数";
- 第三个参数通常是失败时显示的提示信息;
- 比如
assertEquals(5, result, "2+3 应该等于 5")中,第三个参数只有在测试失败时才作为错误提示显示。
5. Maven 中的 scope=test
-
它是什么
scope=test表示这个依赖只在测试范围内生效。
-
有什么作用
- JUnit 是测试框架,只应该给测试代码使用;
- 正式业务代码不应该依赖 JUnit。
-
它的原理
src/test/java下的测试代码可以使用这个依赖;src/main/java下的主程序代码不应该使用这个依赖;- 打正式运行包时,这类测试依赖通常不会作为运行依赖参与进去。
-
初学者容易混淆的点
scope=test不是"让源代码中也执行测试";- 它是限制 JUnit 只在测试阶段可用,避免污染主程序。
三、重难点
1. 为什么 JUnit 测试不需要 main 方法
-
结论
- 因为 JUnit 自己有测试引擎,会自动找到并执行测试方法。
-
原因
- 我们平时写普通 Java 程序,需要
main方法作为入口; - 但测试代码是由 JUnit 框架启动的;
- JUnit 会扫描带
@Test的方法,把它们当作测试入口。
- 我们平时写普通 Java 程序,需要
-
比喻
- 普通程序像自己开门进房间,
main是门; - JUnit 测试像老师点名,谁身上有
@Test标记,老师就叫谁出来答题。
- 普通程序像自己开门进房间,
2. @Test 不能随便去掉
-
结论
- 测试方法必须用
@Test标记,JUnit 才会识别它。
- 测试方法必须用
-
原因
- JUnit 判断一个方法是不是测试方法,主要看注解;
- 不是只看方法名。
-
例子
java
@Test
void testAddition() {
int result = 2 + 3;
assertEquals(5, result);
}
如果去掉 @Test:
java
void testAddition() {
int result = 2 + 3;
assertEquals(5, result);
}
这个方法就只是普通方法,不会被 JUnit 自动执行。
3. assertEquals 的三个参数
-
结论
assertEquals(expected, actual, message)用来判断实际值是否等于期望值。
-
原因
- 单元测试最核心的思想就是:给定输入,检查输出是否符合预期。
-
例子
java
assertEquals(5, result, "2+3 应该等于 5");
含义:
5:期望值;result:实际值;"2+3 应该等于 5":失败时显示的提示信息。
注意:
第三个参数不是"要执行的代码",而是测试失败时的说明文字。
4. assertThrows 是专门测试异常的
-
结论
assertThrows用来判断某段代码是否会抛出指定类型的异常。
-
原因
- 有些方法在遇到非法数据时,本来就应该抛异常;
- 这时候"抛出正确异常"反而说明代码是对的。
-
例子
java
assertThrows(ArithmeticException.class, () -> {
int x = 1 / 0;
});
这段测试的意思是:
我预期这段代码会抛出
ArithmeticException。
因为整数 1 / 0 确实会抛出 ArithmeticException,所以测试通过。
如果这段代码没有抛异常,或者抛出的不是 ArithmeticException,测试才会失败。
- 比喻
- 普通断言是在检查"答案是不是对";
assertThrows是在检查"该报错的时候有没有报正确的错"。
5. assertAll 是批量断言
-
结论
assertAll可以把多个断言组合成一组一起执行。
-
原因
- 普通多个断言顺序执行时,如果前面的断言失败,后面的断言可能不再执行;
assertAll会尽量执行组内所有断言,并汇总失败信息。
-
例子
java
assertAll("user",
() -> assertEquals("Alice", getUserName()),
() -> assertNotNull(getUserAge())
);
含义:
-
这一组断言的名字叫
user; -
第一个断言检查用户名是不是
Alice; -
第二个断言检查年龄是不是不为
null。 -
比喻
- 普通断言像检查作业时发现第一题错了就停下;
assertAll像把整张卷子都检查完,再告诉你哪些题错了。
6. 测试代码应该放在 src/test/java
-
结论
- 测试类应该放在
src/test/java,业务代码放在src/main/java。
- 测试类应该放在
-
原因
- 这是 Maven 的标准项目结构;
- Maven 会把主程序和测试程序分开处理;
- 测试代码不会混进正式业务代码里。
-
例子
text
src/main/java 放正式代码
src/test/java 放测试代码
- 比喻
src/main/java是正式作业;src/test/java是检查作业用的验算纸。
7. 企业开发中要测边界值
-
结论
- 单元测试不能只测正常情况,还要测异常情况和边界情况。
-
原因
- 很多 bug 都出现在特殊输入上,比如
null、空字符串、长度不对、极大值、极小值。
- 很多 bug 都出现在特殊输入上,比如
-
例子
- 如果测试加法方法,不能只测:
java
2 + 3 = 5
- 还可以测:
text
0 + 0
-1 + 1
-2 + -3
Integer.MAX_VALUE + 1
Integer.MIN_VALUE - 1
注意:
如果方法参数是
int,就不能传入字符或字符串;字符、字符串适合测试参数类型为String的方法。
四、代码理解
1. CalculatorTest.java 整体结构
java
package com.test;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void testAddition() {
int result = 2 + 3;
assertEquals(5, result, "2+3 应该等于 5");
}
@Test
void testDivideByZero() {
assertThrows(ArithmeticException.class, () -> {
int x = 1 / 0;
});
}
@Test
void testMultipleAssertions() {
assertAll("user",
() -> assertEquals("Alice", getUserName()),
() -> assertNotNull(getUserAge())
);
}
private String getUserName() { return "Alice"; }
private Integer getUserAge() { return 25; }
}
关键行理解:
-
import org.junit.jupiter.api.Test;- 导入 JUnit 5 的
@Test注解。
- 导入 JUnit 5 的
-
import static org.junit.jupiter.api.Assertions.*;- 静态导入断言方法;
- 这样就可以直接写
assertEquals(),不用写Assertions.assertEquals()。
-
@Test- 标记下面的方法是测试方法。
-
assertEquals(5, result, "2+3 应该等于 5")- 判断
result是否等于5; - 不等于就测试失败,并显示提示信息。
- 判断
-
assertThrows(ArithmeticException.class, () -> { int x = 1 / 0; })- 判断代码块是否抛出算术异常;
- 抛出了指定异常,测试通过。
-
assertAll(...)- 把多个断言合成一组执行。
2. 常见断言方法
java
assertEquals(期望值, 实际值, 失败提示);
assertNotEquals(不期望的值, 实际值, 失败提示);
assertNull(实际对象, 失败提示);
assertNotNull(实际对象, 失败提示);
assertTrue(条件, 失败提示);
assertFalse(条件, 失败提示);
assertThrows(异常类型.class, 要执行的代码, 失败提示);
记忆重点:
Equals:判断相等;NotEquals:判断不相等;Null:判断为null;NotNull:判断不为null;True:判断条件为真;False:判断条件为假;Throws:判断会不会抛出指定异常。
3. 常见注解
java
@Test
标记普通测试方法。
java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
参数化测试,让同一个测试方法用多组参数运行。
java
@BeforeEach
每个测试方法执行前运行一次。
java
@AfterEach
每个测试方法执行后运行一次。
java
@BeforeAll
所有测试方法执行前只运行一次,通常用于全局初始化。
java
@AfterAll
所有测试方法执行后只运行一次,通常用于全局清理。
4. 参数化测试示例
java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testPositiveNumber(int number) {
assertTrue(number > 0);
}
这段代码会执行三次:
text
number = 1
number = 2
number = 3
注意:
使用
@ParameterizedTest时,不需要再加@Test。
5. 测试前后执行的方法
java
@BeforeEach
void setUp() {
System.out.println("每个测试方法前执行");
}
@AfterEach
void tearDown() {
System.out.println("每个测试方法后执行");
}
如果一个测试类中有 3 个 @Test 方法:
@BeforeEach会执行 3 次;@AfterEach会执行 3 次。
java
@BeforeAll
static void beforeAll() {
System.out.println("所有测试前只执行一次");
}
@AfterAll
static void afterAll() {
System.out.println("所有测试后只执行一次");
}
如果一个测试类中有 3 个 @Test 方法:
@BeforeAll只执行 1 次;@AfterAll只执行 1 次。
五、易错点
-
以为 JUnit 测试必须写
main方法- JUnit 测试不需要自己写
main; - JUnit 测试引擎会自动执行带
@Test的方法。
- JUnit 测试不需要自己写
-
去掉
@Test导致方法不执行- 方法名叫
testAddition不够; - 必须加
@Test才能被 JUnit 识别。
- 方法名叫
-
误解
assertEquals的第三个参数- 第三个参数是失败提示信息;
- 不是实际值不等于期望值时要执行的代码。
-
不理解
assertThrows为什么通过- 如果预期某段代码应该抛异常,并且它确实抛出了指定异常,测试就是通过。
-
不理解
assertAll的作用assertAll是把多个断言组合起来,尽量全部执行后统一汇总结果。
-
把测试代码放到
src/main/java- 测试代码应该放在
src/test/java; - 业务代码应该放在
src/main/java。
- 测试代码应该放在
-
JUnit 依赖没有设置
scope=test- JUnit 是测试依赖,不应该成为主程序运行依赖。
-
只测试正常情况,不测试边界情况
- 企业开发中,
null、空值、边界值、异常值都很重要。
- 企业开发中,
六、记忆口诀 / 通俗比喻
1. 单元测试口诀
一个方法一个测,结果对错断言说。
2. JUnit 口诀
@Test一贴,JUnit 来测。
3. 断言口诀
期望在前,实际在后,错了提示跟最后。
4. assertThrows 口诀
该报错时报对错,就是通过。
5. assertAll 比喻
普通断言像错一题就停;
assertAll像整张卷子都批完再汇总。
6. Maven 测试目录比喻
main放正式作业,test放验算草稿。
七、应用
在实际开发中,单元测试主要用来保证方法的正确性。比如写一个用户注册功能,不能只测试正常注册成功,还要测试用户名为空、密码为空、手机号格式错误、用户已存在等情况。写完业务方法后,把测试类放到 src/test/java,用 @Test 标记测试方法,用 assertEquals、assertTrue、assertThrows 等断言检查结果。如果修改了业务代码,再运行测试,就能快速知道有没有把原来的功能改坏。JUnit 配合 Maven 的 scope=test 使用,可以让测试代码和正式业务代码分开,项目结构更清楚,也更符合企业开发规范。
八、最终总结
JUnit 是 Java 中常用的单元测试框架,用来测试方法是否按预期工作。测试方法不需要 main,只要加上 @Test,JUnit 测试引擎就能自动识别并执行。断言是单元测试的核心,assertEquals 判断结果是否相等,assertThrows 判断是否抛出指定异常,assertAll 可以批量执行多个断言。测试代码应该放在 src/test/java,JUnit 依赖应该设置 scope=test。企业开发中写测试不能只测正常情况,还要覆盖异常值和边界值。