如何在JUnit中测试JPA保存时的javax.validation约束校验
校验注解搭配规则
字符串字段
仅使用@Size无法覆盖全部校验场景,三类注解的校验边界完全不同:
@NotBlank:仅针对字符串类型,校验值不能为null,且去除首尾空白后长度大于0,适合人名、标题这类需要实际有效内容的字段@NotEmpty:针对字符串、集合、数组、Map类型,校验值不能为null且长度/大小大于0,但不会处理字符串首尾全空格的场景,全空格字符串会被判定为合法@Size:仅校验长度/大小是否在min、max区间内,对null值完全不生效。空字符串""长度为0,配置min=2时确实会被拦截,但传入null时不会触发任何校验,会直接落到数据库层抛非空约束错误。
针对示例中的name字段,正确组合是@NotBlank + @Size(min = 2, max = 50),不需要额外加@NotEmpty,@NotBlank已经覆盖了非空校验逻辑。
数值字段
必须同时配置@NotNull和@PositiveOrZero:@PositiveOrZero同样对null值不生效,只配置该注解时,传入null会绕过Bean Validation,直接触发数据库非空报错。
之前测试saveAndFlush只拿到数据库层DataIntegrityViolationException,核心原因就是缺少@NotBlank、@NotNull注解,非法值绕过了Bean Validation层直接落到数据库。
测试代码问题修正
当前遇到的异常出现在@AfterEach阶段、测试方法内不抛预期异常,核心原因有三个:
- 测试注解冲突:同时使用了JUnit4的
@RunWith(SpringRunner.class)、Mockito的@ExtendWith(MockitoExtension.class)和@DataJpaTest,三个注解作用重叠冲突。使用的@BeforeEach、@AfterEach是JUnit5注解,直接保留@DataJpaTest即可,不需要额外加另外两个注解——Repository层集成测试不需要Mockito mock任何组件。 - JPA持久化上下文延迟刷出机制:调用
repository.save()时,JPA仅将实体放入持久化上下文缓存,不会立刻执行SQL、也不会触发Bean Validation,只有上下文flush时才会同步数据到数据库、执行校验。之前在测试方法内save非法实体后没有触发flush,非法实体一直留在缓存中,直到@AfterEach调用deleteAll()时JPA为了同步删除操作先flush缓存,才在销毁阶段抛出校验异常。 - try-catch吞异常的写法不可靠:如果save操作没有抛出预期异常,测试会误判为通过,必须用
assertThrows做显式断言。
推荐测试方案
校验规则测试不需要依赖Spring容器和数据库,直接用Validator做纯单元测试即可,执行速度最快,也不会遇到JPA缓存导致的各种问题:
import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; class PersonValidationTest { private Validator validator; @BeforeEach void setUp() { try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { validator = factory.getValidator(); } } @Test void shouldFailWhenNameIsBlank() { Person person = new Person("", 40.0d); Set<ConstraintViolation<Person>> violations = validator.validate(person); assertEquals(1, violations.size()); assertEquals("First name can't be empty.", violations.iterator().next().getMessage()); } @Test void shouldFailWhenNameShorterThan2() { Person person = new Person("A", 40.0d); Set<ConstraintViolation<Person>> violations = validator.validate(person); assertEquals(1, violations.size()); assertEquals("First name can't be longer than 50 symbols.", violations.iterator().next().getMessage()); } @Test void shouldFailWhenWeightIsNull() { Person person = new Person("ValidName", null); Set<ConstraintViolation<Person>> violations = validator.validate(person); assertEquals(1, violations.size()); assertEquals("Weight is required.", violations.iterator().next().getMessage()); } @Test void shouldFailWhenWeightIsNegative() { Person person = new Person("ValidName", -1.0d); Set<ConstraintViolation<Person>> violations = validator.validate(person); assertEquals(1, violations.size()); assertEquals("Weight should be positive number or zero.", violations.iterator().next().getMessage()); } }
注:Spring Boot 2.x版本使用javax.validation包下的对应类即可,逻辑完全一致。
集成测试修正方案
如果需要验证Repository层持久化流程中校验确实生效,可以注入EntityManager手动控制flush和缓存清理,避免非法实体残留到销毁阶段:
import jakarta.persistence.EntityManager; import jakarta.validation.ConstraintViolationException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.assertThrows; @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class PersonRepositoryTest { @Autowired private PersonRepository personRepository; @Autowired private EntityManager entityManager; @BeforeEach void initUseCase() { List<Person> people = Arrays.asList( new Person("One", 40.0d), new Person("Two", 40.0d) ); personRepository.saveAll(people); entityManager.flush(); entityManager.clear(); } @AfterEach void destroyAll(){ personRepository.deleteAll(); entityManager.flush(); entityManager.clear(); } @Test void shouldThrowConstraintExceptionWhenSaveInvalidPerson() { Person person = new Person("", 40.0d); personRepository.save(person); assertThrows(ConstraintViolationException.class, () -> entityManager.flush()); // 清空缓存移除非法实体,避免销毁阶段报错 entityManager.clear(); } }
内容的提问来源于stack exchange,提问作者Deyvid Dimitrov
相关产品推荐
相关产品推荐

