
本文讲解当Java Bean字段被@NotNull等JSR-303校验注解约束时,如何在JUnit单元测试中安全模拟null输入以验证参数校验逻辑,避免因构造阶段触发非预期的NullPointerException。
本文讲解当java bean字段被`@notnull`等jsr-303校验注解约束时,如何在junit单元测试中安全模拟`null`输入以验证参数校验逻辑,避免因构造阶段触发非预期的`nullpointerexception`。
在Spring或Jakarta EE项目中,使用@NotNull、@NotEmpty等校验注解(来自jakarta.validation或旧版javax.validation)是一种常见做法,用于声明字段的业务约束。然而,这些注解本身不会自动拦截赋值操作——它们仅在显式调用校验器(如Validator.validate())时生效。你遇到的错误:
java.lang.NullPointerException: endDate is marked non-null but is null
并非由@NotNull触发,而是由Lombok的@Setter生成的 setter 方法内部的隐式空值检查导致的。Lombok自2.0+版本起默认启用@NonNull语义支持:当字段被@NotNull(或同名注解)标注时,Lombok会为对应setter自动生成非空断言(即类似 if (endDate == null) throw new NullPointerException("endDate is marked non-null but is null"))。这是Lombok的主动防护行为,与JSR-303校验无关。
✅ 正确的修复方式
要让测试能传入null值并交由你的业务逻辑(如@Valid控制器校验)统一处理,必须禁用Lombok对@NotNull的setter空值拦截。推荐两种方案:
方案1:移除Lombok的@NotNull依赖(推荐)
将字段上的@NotNull替换为纯校验注解(不参与Lombok代码生成),例如使用jakarta.validation.constraints.NotBlank(适用于字符串)或@NotNull但确保其来源是jakarta.validation.constraints而非Lombok感知的注解,并显式关闭Lombok的@NonNull推导:
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChartDataSearchCondition {
@NotBlank(message = "startDate must not be blank")
@Pattern(regexp = Constants.REGEX_CHART_CONDITION_DATE, message = "startDate format invalid")
private String startDate;
@NotBlank(message = "endDate must not be blank")
@Pattern(regexp = Constants.REGEX_CHART_CONDITION_DATE, message = "endDate format invalid")
private String endDate;
@NotNull(message = "columns must not be null")
@NotEmpty(message = "columns must not be empty")
private List<String> columns;
}并在lombok.config文件中添加(防止Lombok误判):
lombok.anyConstructor.addConstructorProperties = false lombok.setter.flagUsage = WARNING // 或 ERROR,避免误用 # 关键:禁用Lombok对jakarta.validation注解的setter空检查 lombok.nonNull.failOnNull = false
方案2:手动编写setter(完全可控)
若需彻底规避Lombok干预,可移除@Setter,手写setter方法,跳过空值检查:
public void setEndDate(String endDate) {
this.endDate = endDate; // 不做null检查,交由后续校验器处理
}方案3:测试时绕过setter(快速临时解法)
在测试中直接通过反射或构造器初始化对象(不调用setter):
@Test
public void visualize_searchConditionWithEndDateIsNull() {
List<String> columns = List.of("col2");
// 使用反射绕过Lombok生成的带空检查的setter
ChartDataSearchCondition condition = new ChartDataSearchCondition();
try {
Field endDateField = condition.getClass().getDeclaredField("endDate");
endDateField.setAccessible(true);
endDateField.set(condition, null); // 强制设为null
Field startDateField = condition.getClass().getDeclaredField("startDate");
startDateField.setAccessible(true);
startDateField.set(condition, "2021-06-01 19:00:00.000+0700");
Field columnsField = condition.getClass().getDeclaredField("columns");
columnsField.setAccessible(true);
columnsField.set(condition, columns);
// 调用目标方法(此时endDate为null,等待@Valid触发校验)
controller.getTimeseriesSampling(cluster, database, container, condition);
Assert.fail("Expected validation exception");
} catch (Exception e) {
// 预期捕获校验失败异常
if (e instanceof CommonException ce &&
ErrorCode.CHART_REQUEST_INVALID.getErrorCode().equals(ce.getCode())) {
return;
}
throw e;
}
}⚠️ 注意:反射方式仅建议用于紧急验证,长期应采用方案1或2以保持代码清晰与可维护性。
✅ 最佳实践总结
- ✅ @NotNull等校验注解应仅用于声明式校验,不应承担运行时空值防护职责;
- ✅ Lombok的@Setter + @NotNull组合易引发混淆,建议分离关注点:Lombok负责便捷访问,jakarta.validation负责业务校验;
- ✅ 单元测试中验证null场景时,确保对象构建过程不提前抛出异常,把校验时机留给@Valid或显式validator.validate()调用;
- ✅ Spring MVC控制器中,务必配合@Valid或@Validated使用,才能真正触发JSR-303校验逻辑并返回MethodArgumentNotValidException或自定义异常。
通过以上调整,你的测试即可成功传入null值,并由业务层统一捕获和响应校验失败,既符合契约设计,也保障了测试完整性。

















