核心是通过深度比对业务字段、不可变设计和防御性拷贝保障状态一致性:用AssertJ的usingRecursiveComparison()忽略无关字段递归比对;用record或@Value实现不可变对象;Service层显式新建实例并验证引用不同;对衍生字段需单独断言一致性。

核心思路是:不依赖对象引用相等,而是通过深度比对关键业务字段,结合不可变设计和防御性拷贝来保障状态一致性。
用 AssertJ 的 usingRecursiveComparison() 做字段级深度断言
这是最直接有效的方式,能自动忽略无关字段(如临时 ID、时间戳),聚焦业务状态:
- 原始对象和处理后对象都调用 assertThat(actual).usingRecursiveComparison().ignoringFields("id", "updatedAt").isEqualTo(expected)
- 支持嵌套对象、集合、Map 的递归比对,无需手写 equals 或重载 toString
- 失败时清晰提示哪个字段/路径不一致(例如:"order.items[0].price" expected: 99.9 but was: 89.9)
为关键业务对象启用不可变(Immutable)设计
从源头杜绝意外修改,让“状态未被篡改”成为编译期保障:
- 用 record(Java 14+)定义 DTO 或领域对象,天然 final 字段 + 不可变构造
- 或使用 Lombok 的 @Value(注意排除非业务字段,如 @Value.Exclude)
- Service 层接收 record / @Value 对象后,若需转换,应显式 new 新实例,而非复用原引用
在 Service 接口契约中明确“是否返回新对象”
避免隐式共享状态。测试前先确认行为约定:
立即学习“Java免费学习笔记(深入)”;
- 如果接口文档写明 "返回新订单对象,不修改入参",则测试必须验证 input != output(引用不同)且字段相同
- 如果方法内部做了防御性拷贝(如 new Order(input)),测试中可用 assertThat(output).isNotSameAs(input) 验证
- 对 List 等集合字段,检查 output.getItems() != input.getItems(),再比内容
对含计算逻辑的字段,单独断言其衍生一致性
有些字段不是直接赋值,而是由其他字段推导得出(如 totalAmount = sum(items.price * qty)),需额外校验:
- 先用 recursiveComparison 验证输入字段(items、discount 等)未变
- 再单独 assert:assertThat(output.getTotalAmount()).isEqualTo(calculateExpectedTotal(input))
- 避免把“总金额没变”当作“所有字段都没变”的替代证据


















