JUnit 5 的断言怎么写?assertThrows、assertAll 和 AssertJ 的链式断言有什么优势?
简化版
**断言(Assertion)是单元测试的核心——「验证代码的实际结果是否符合预期」,不符合就让测试失败。**JUnit 5 的内置断言(org.junit.jupiter.api.Assertions):① 基本断言——assertEquals(expected, actual)(相等)、assertTrue/assertFalse(布尔)、assertNull/assertNotNull;② assertThrows——验证「某段代码会抛出指定异常」(assertThrows(异常类, () -> 方法()),还能拿到异常对象校验消息);③ assertAll——把多个断言「分组一起执行」,即使前面的断言失败,后面的也会执行(一次报告所有失败,而不是遇到第一个失败就停);④ assertTimeout——验证执行时间。AssertJ(第三方库) 提供更强大的「链式断言」——assertThat(actual).isEqualTo(x).isNotNull(),可读性极强(像自然语言)、支持丰富的断言方法(集合、字符串、异常、对象等)、失败信息更清晰。为什么用 AssertJ:JUnit 内置断言够用但表达力有限(assertEquals(expected, actual) 参数顺序容易搞反),AssertJ 的 assertThat(实际值).满足条件 更直观、链式、失败信息更友好。核心:JUnit 5 断言够用(assertThrows 测异常、assertAll 一次报告所有失败),AssertJ 链式断言可读性和表达力更强(推荐)。
详细版
JUnit 5 常用断言:
| 断言 | 作用 |
|---|---|
| assertEquals(exp, act) | 相等 |
| assertTrue/assertFalse | 布尔 |
| assertNull/assertNotNull | 空/非空 |
| assertThrows(异常类, 执行) | 验证抛出异常 |
| assertAll(断言…) | 分组执行、一次报告所有失败 |
| assertTimeout | 验证执行时间 |
| assertArrayEquals | 数组相等 |
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
// JUnit 5 基本断言
@Test void basic() {
assertEquals(5, calc.add(2, 3)); // 相等
assertTrue(user.isActive()); // 布尔
assertNotNull(result); // 非空
}
// assertThrows:验证抛异常
@Test void throwsException() {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> service.validate(null)); // 期望这段抛 IllegalArgumentException
assertEquals("参数不能为空", ex.getMessage()); // 校验异常消息
}
// assertAll:一次报告所有失败(即使前面失败,后面也执行)
@Test void allAssertions() {
assertAll("user",
() -> assertEquals("Tom", user.getName()),
() -> assertEquals(18, user.getAge()),
() -> assertNotNull(user.getEmail())); // 三个都执行,一次报告所有失败
}
// AssertJ 链式断言(可读性强,推荐)
@Test void assertJ() {
assertThat(user.getName()).isEqualTo("Tom").isNotBlank(); // 链式
assertThat(list).hasSize(3).contains("a").doesNotContain("z"); // 集合
assertThat(exception).hasMessageContaining("参数"); // 异常
}
⚠️ 两个值得记的点:JUnit 断言的「参数顺序坑」和 AssertJ 的「链式可读性优势」。JUnit 的
assertEquals(expected, actual)参数顺序是「期望值在前、实际值在后」——很容易搞反,搞反了断言逻辑还是对的(相等就是相等),但失败信息会误导(「expected: 实际值 but was: 期望值」,看着别扭)。AssertJ 从根本上避免了这个问题——assertThat(actual).isEqualTo(expected),实际值在前(assertThat里)、期望值在后,语义自然(「断言 实际值 等于 期望值」),不会搞反。而且 AssertJ 链式 + 语义化方法名读起来像自然语言(assertThat(list).hasSize(3).contains("a")),失败信息也更清晰(明确指出哪个断言失败、期望什么、实际什么)。所以推荐用 AssertJ(表达力强、可读性好、不易出错)。另外assertAll很实用——普通断言遇到第一个失败就停(后面的不执行、看不到还有哪些问题),assertAll把多个断言分组、都执行、一次报告所有失败(一次看清所有问题)。
完整版教学
一、断言是测试的核心
先理解断言的作用:
单元测试的结构(AAA/Given-When-Then):
① Arrange/Given:准备(构造对象、mock 依赖)
② Act/When:执行(调用被测方法)
③ Assert/Then:断言(验证结果符合预期)
断言(Assert)的作用:
验证"实际结果"是否符合"预期"
符合 → 断言通过
不符合 → 断言失败 → 测试失败(报告哪里不符合)
没有断言的测试是无意义的:
只调用方法、不验证结果 → 测不出对错
→ 断言是测试的核心(验证行为是否正确)
断言的类型:
① 值断言:结果等于预期(assertEquals)
② 状态断言:对象的状态正确(assertTrue(user.isActive()))
③ 异常断言:抛出预期的异常(assertThrows)
④ 交互断言:mock 的验证(verify,见 Mockito 题)
所以断言 = 验证实际结果符合预期(测试的核心)
断言是测试的核心——单元测试结构(Arrange/Given 准备、Act/When 执行、Assert/Then 断言)。断言的作用:验证实际结果是否符合预期(符合通过、不符合失败报告哪里不符合)。没有断言的测试无意义(只调用不验证测不出对错、断言是核心)。断言类型:值断言(assertEquals)、状态断言(assertTrue)、异常断言(assertThrows)、交互断言(verify)。理解「断言验证实际结果符合预期(测试核心)、AAA 结构 Arrange/Act/Assert、没断言的测试无意义、类型:值/状态/异常/交互断言」,就理解了断言的作用。
二、JUnit 5 基本断言
理解 JUnit 5 的基本断言:
JUnit 5 基本断言(Assertions 类的静态方法):
assertEquals(expected, actual):相等(对象用 equals 比)
assertNotEquals(a, b):不相等
assertTrue(condition):为真
assertFalse(condition):为假
assertNull(obj):为 null
assertNotNull(obj):非 null
assertSame(a, b):同一个对象(==)
assertArrayEquals(exp, act):数组相等
assertIterableEquals:可迭代相等
带消息的断言(失败时显示):
assertEquals(5, result, "计算结果应该是 5")
→ 失败时显示这个消息(帮助定位)
★ 参数顺序坑:
assertEquals(expected, actual) —— 期望值在前、实际值在后
容易搞反!搞反了逻辑对(相等就是相等),但失败信息误导
→ 记住:expected 在前
浮点数比较:
assertEquals(0.3, a + b, 0.0001) // 第三个参数是误差范围
→ 浮点数不能直接 equals(精度问题)
所以 JUnit 5 基本断言:assertEquals/assertTrue/assertNull 等,注意参数顺序
JUnit 5 基本断言(Assertions 类静态方法):assertEquals(相等,对象用 equals)、assertTrue/assertFalse(布尔)、assertNull/assertNotNull、assertSame(==)、assertArrayEquals。带消息的断言(失败时显示帮定位)。参数顺序坑:assertEquals(expected, actual) 期望值在前、实际值在后、容易搞反(搞反逻辑对但失败信息误导)。浮点数比较用第三个误差参数(assertEquals(0.3, a+b, 0.0001),浮点不能直接 equals)。理解「JUnit 5 基本断言 assertEquals/assertTrue/assertNull;带消息断言;★参数顺序 expected 在前容易搞反;浮点数用误差参数比较」,就掌握了基本断言。
三、assertThrows:测异常
assertThrows——验证抛出异常:
assertThrows:验证"某段代码会抛出指定异常"
ThrowableType ex = assertThrows(异常类.class, () -> 会抛异常的代码);
→ 期望这段代码抛出这个异常
→ 抛了 → 断言通过,返回异常对象(可进一步校验)
→ 没抛/抛了别的 → 断言失败
例:
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> service.validate(null)); // 期望抛 IllegalArgumentException
// 拿到异常对象,进一步校验
assertEquals("参数不能为空", ex.getMessage()); // 校验消息
assertTrue(ex.getMessage().contains("参数"));
对比 JUnit 4 的老写法(不好):
JUnit 4:@Test(expected = XxxException.class)
→ 只能验证"整个方法抛异常",不能精确到某行、不能校验消息
JUnit 5 assertThrows:
→ 精确验证某段代码、能拿异常对象校验消息(更好)
assertDoesNotThrow:
验证"某段代码不抛异常"
assertDoesNotThrow(() -> service.process());
为什么要测异常:
异常也是行为的一部分——某些输入应该抛异常(如非法参数)
→ 要验证"该抛异常时抛了、抛的是对的异常、消息对"
所以 assertThrows 验证抛出指定异常、返回异常对象可校验消息
assertThrows 验证抛出异常——assertThrows(异常类, () -> 代码)(期望这段代码抛这个异常、抛了返回异常对象可进一步校验、没抛/抛别的失败)。例:拿到异常对象校验 getMessage。对比 JUnit 4 的 @Test(expected=...)(只能验证整个方法抛异常、不能精确/不能校验消息,JUnit 5 更好)。assertDoesNotThrow 验证不抛异常。为什么测异常:异常也是行为的一部分(某些输入应该抛异常)、要验证该抛时抛了+抛的对+消息对。理解「assertThrows 验证抛出指定异常(返回异常对象可校验消息)、比 JUnit 4 @Test(expected)好(精确+能校验消息);assertDoesNotThrow 验证不抛;异常是行为一部分要验证」,就掌握了 assertThrows。
四、assertAll:一次报告所有失败
assertAll——分组断言、一次报告所有失败:
问题:普通断言遇到第一个失败就停
assertEquals("Tom", user.getName()); // 如果这个失败
assertEquals(18, user.getAge()); // 这个不执行了(看不到)
assertNotNull(user.getEmail()); // 也不执行
→ 只知道"name 错了",不知道 age、email 有没有问题
→ 要修完 name 重跑才能看到下一个问题(低效)
assertAll:把多个断言分组、都执行、一次报告所有失败
assertAll("user",
() -> assertEquals("Tom", user.getName()),
() -> assertEquals(18, user.getAge()),
() -> assertNotNull(user.getEmail()));
→ 三个都执行(即使前面失败)
→ 一次报告所有失败的(如"name 错、age 错"一起报)
→ 一次看清所有问题(高效)
好处:
① 一次看到所有断言的结果(不用逐个修)
② 适合验证一个对象的多个属性
什么时候用:
验证一个对象/结果的多个方面(多个相关断言)
→ 用 assertAll 分组,一次报告
所以 assertAll 分组断言、都执行、一次报告所有失败(一次看清所有问题)
assertAll 分组断言、一次报告所有失败——问题:普通断言遇到第一个失败就停(后面的不执行、看不到、只知道第一个错、要修完重跑才看下一个、低效)。assertAll 把多个断言分组、都执行、一次报告所有失败(即使前面失败后面也执行、一次报告所有失败的、一次看清所有问题)。好处:一次看到所有断言结果、适合验证一个对象的多个属性。理解「assertAll 分组断言都执行一次报告所有失败;问题:普通断言遇第一个失败就停(后面不执行看不到);好处一次看清所有问题、适合验证一个对象多个属性」,就掌握了 assertAll。
五、AssertJ:链式断言
AssertJ——更强大的链式断言,推荐:
AssertJ(第三方断言库):
提供 assertThat(实际值).满足条件 的链式断言
→ 可读性强、表达力丰富、失败信息清晰
基本用法:
assertThat(actual).isEqualTo(expected);
→ "断言 actual 等于 expected"(语义自然,实际值在前)
链式:
assertThat(user.getName())
.isNotNull()
.isEqualTo("Tom")
.startsWith("T");
→ 一个值多个断言链式(可读)
丰富的断言(各类型):
字符串:isEqualTo、contains、startsWith、isBlank、hasSize...
集合:hasSize、contains、containsExactly、isEmpty、doesNotContain...
数字:isGreaterThan、isBetween、isPositive...
对象:hasFieldOrProperty、isInstanceOf、usingRecursiveComparison...
异常:assertThatThrownBy(() -> ...).isInstanceOf(...).hasMessage(...)
Optional、Map、日期... 都有丰富断言
优势(对比 JUnit 断言):
① 可读性——像自然语言(assertThat(list).hasSize(3).contains("a"))
② 不易出错——实际值在 assertThat 里(不会搞反 expected/actual)
③ 表达力强——丰富的语义化方法(集合、字符串等专门断言)
④ 失败信息清晰——明确哪个断言失败、期望/实际是什么
⑤ IDE 友好——链式调用有自动补全
异常断言(AssertJ 风格):
assertThatThrownBy(() -> service.validate(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("参数");
→ 比 assertThrows 更链式
所以 AssertJ 链式断言可读性/表达力/失败信息更强(推荐)
AssertJ 提供 assertThat(实际值).满足条件 的链式断言(可读性强、表达力丰富、失败信息清晰)。基本用法 assertThat(actual).isEqualTo(expected)(实际值在前语义自然)。链式(一个值多个断言)。丰富的断言(字符串 contains/hasSize、集合 containsExactly、数字 isGreaterThan、对象、异常 assertThatThrownBy)。优势(对比 JUnit):可读性(像自然语言)、不易出错(实际值在 assertThat 里不会搞反)、表达力强、失败信息清晰、IDE 友好。理解「AssertJ 链式断言 assertThat(实际值).满足条件;丰富断言(字符串/集合 containsExactly/数字/异常 assertThatThrownBy);优势:可读性(自然语言)+不易出错(实际值在前)+表达力+失败信息清晰;推荐」,就掌握了 AssertJ。
六、实践与选择
总结断言的实践和选择:
选择:
① JUnit 内置断言:简单、无额外依赖(基本够用)
assertEquals、assertThrows、assertAll
② AssertJ:可读性强、表达力好(推荐,尤其复杂断言)
assertThat(...).链式
→ 简单项目用 JUnit 断言、追求可读性/复杂断言用 AssertJ
实践建议:
① 用 assertThrows 测异常(比 JUnit 4 好、能校验消息)
② 用 assertAll 验证一个对象的多个属性(一次报告所有失败)
③ 复杂断言(集合、对象、字符串)用 AssertJ(表达力强)
④ 断言要有意义(验证行为,别只 assertNotNull)
⑤ 失败信息清晰(用带消息的断言,或 AssertJ 天然清晰)
常见断言库:
JUnit 5 Assertions:内置
AssertJ:流式断言(最流行、推荐)
Hamcrest:matcher 风格(assertThat + matcher,较老)
Truth:Google 的(类似 AssertJ)
核心总结:
断言验证实际结果符合预期(测试核心)
JUnit 5:assertEquals/assertThrows(测异常)/assertAll(一次报告所有失败)
AssertJ:链式断言(可读性/表达力/失败信息更强,推荐)
assertEquals 参数 expected 在前(易搞反)、AssertJ 实际值在前(不搞反)
断言选择:JUnit 内置断言(简单无依赖、基本够用)、AssertJ(可读性强表达力好、推荐尤其复杂断言)。实践:assertThrows 测异常、assertAll 验证多属性一次报告所有失败、复杂断言用 AssertJ、断言要有意义、失败信息清晰。常见库:JUnit 5 Assertions(内置)、AssertJ(最流行推荐)、Hamcrest(matcher 风格较老)、Truth(Google)。理解「选择:JUnit 内置(简单无依赖)/AssertJ(可读推荐);实践:assertThrows 测异常/assertAll 多属性/复杂用 AssertJ/断言有意义;库:JUnit/AssertJ(推荐)/Hamcrest/Truth」,就掌握了实践与选择。
记忆钩子:「断言验证实际结果符合预期(测试核心);JUnit 5 断言:①基本 assertEquals(expected,actual)/assertTrue/assertNull(★参数顺序 expected 在前容易搞反)②assertThrows(异常类,()->代码)验证抛出指定异常(返回异常对象可校验消息,比 JUnit 4 @Test(expected)好)③assertAll(分组断言都执行一次报告所有失败,普通断言遇第一个失败就停);★AssertJ 链式断言 assertThat(实际值).isEqualTo(x).链式:可读性强(像自然语言)+不易出错(实际值在 assertThat 里不会搞反)+表达力丰富(集合 containsExactly/字符串/异常 assertThatThrownBy)+失败信息清晰,推荐;简单用 JUnit 内置、复杂/追求可读用 AssertJ」。
七、常见误区与追问
- 误区:assertEquals 的参数顺序无所谓。 顺序是 assertEquals(expected, actual)(期望值在前、实际值在后),容易搞反;搞反了断言逻辑还是对的(相等就是相等),但失败信息会误导(expected 和 actual 反了,看着别扭、难定位);AssertJ 的 assertThat(actual).isEqualTo(expected) 从根本避免了这个问题(实际值在前)。
- 误区:测异常要用 try-catch 加 fail()。 不用——JUnit 5 用 assertThrows(异常类, () -> 代码),简洁地验证「某段代码抛出指定异常」,还返回异常对象可以进一步校验消息;比手写 try-catch + fail() 简洁,也比 JUnit 4 的 @Test(expected=…) 好(能精确到某段代码、能校验异常消息)。
- 误区:多个断言遇到第一个失败就该停。 普通断言确实遇到第一个失败就停(后面的不执行、看不到还有哪些问题);但验证一个对象的多个属性时,用 assertAll 把它们分组,都执行、一次报告所有失败,能一次看清所有问题,不用逐个修完重跑;效率更高。
- 误区:AssertJ 只是写法好看,没实质区别。 AssertJ 有实质优势:① 可读性(链式 + 语义化方法名,像自然语言);② 不易出错(实际值在 assertThat 里,不会搞反 expected/actual);③ 表达力强(集合、字符串、对象、异常都有丰富的专门断言,如 containsExactly、usingRecursiveComparison);④ 失败信息更清晰;对复杂断言(集合、对象比较)优势明显。
- 追问:JUnit 5 怎么测试方法会抛出异常? 用 assertThrows(异常类.class, () -> 会抛异常的代码)——它验证这段代码抛出了指定类型的异常,抛了就通过、返回异常对象(可以进一步校验,如 assertEquals(期望消息, ex.getMessage()) 校验异常消息、assertTrue(ex.getMessage().contains(…)) 等);没抛或抛了别的异常就失败;这比 JUnit 4 的 @Test(expected=…) 好(能精确到某段代码而非整个方法、能拿异常对象校验消息)。
- 追问:assertAll 有什么用,什么时候用? assertAll 把多个断言分组、都执行、一次报告所有失败——普通断言遇到第一个失败就停(后面的不执行,你只知道第一个问题,修完重跑才能看下一个);assertAll 让分组内的所有断言都执行,即使前面的失败,后面的也执行,一次报告所有失败的断言;适合验证一个对象/结果的多个属性(如验证 user 的 name、age、email),一次看清所有问题、效率高。
- 追问:AssertJ 相比 JUnit 内置断言有什么优势? ① 可读性——链式调用 + 语义化方法名,读起来像自然语言(assertThat(list).hasSize(3).contains(“a”).doesNotContain(“z”));② 不易出错——实际值在 assertThat() 里、期望值在断言方法里,语义自然、不会搞反 expected/actual;③ 表达力强——针对字符串、集合、数字、对象、异常、Optional、Map 等都有丰富的专门断言(如集合的 containsExactly、对象的 usingRecursiveComparison 递归比较);④ 失败信息更清晰、IDE 自动补全友好;所以复杂断言推荐用 AssertJ。
八、加强记忆
断言(Assertion)是单元测试的核心——验证实际结果是否符合预期。JUnit 5 内置断言:① 基本断言(assertEquals(expected, actual)(参数期望值在前、容易搞反)、assertTrue/assertFalse、assertNull/assertNotNull);② assertThrows(验证「某段代码抛出指定异常」,assertThrows(异常类, () -> 代码),返回异常对象可校验消息,比 JUnit 4 的 @Test(expected=...) 好——能精确到某段代码、能校验消息);③ assertAll(把多个断言分组、都执行、一次报告所有失败,普通断言遇到第一个失败就停、看不到还有哪些问题,assertAll 一次看清所有问题);④ assertTimeout(验证执行时间)。AssertJ(第三方库,推荐) 提供链式断言 assertThat(实际值).isEqualTo(x).链式——优势:可读性强(像自然语言)、不易出错(实际值在 assertThat 里、不会搞反 expected/actual)、表达力丰富(集合 containsExactly、字符串、异常 assertThatThrownBy、对象递归比较)、失败信息清晰。选择:简单用 JUnit 内置断言、复杂/追求可读性用 AssertJ。一句话「断言验证实际结果符合预期;JUnit 5:assertEquals(expected 在前易搞反)/assertThrows(测异常返回异常对象校验消息)/assertAll(一次报告所有失败);AssertJ 链式断言 assertThat(实际值).满足条件(可读+不易搞反+表达力强,推荐);简单用 JUnit、复杂用 AssertJ」。