You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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阶段、测试方法内不抛预期异常,核心原因有三个:

  1. 测试注解冲突:同时使用了JUnit4的@RunWith(SpringRunner.class)、Mockito的@ExtendWith(MockitoExtension.class)和@DataJpaTest,三个注解作用重叠冲突。使用的@BeforeEach、@AfterEach是JUnit5注解,直接保留@DataJpaTest即可,不需要额外加另外两个注解——Repository层集成测试不需要Mockito mock任何组件。
  2. JPA持久化上下文延迟刷出机制:调用repository.save()时,JPA仅将实体放入持久化上下文缓存,不会立刻执行SQL、也不会触发Bean Validation,只有上下文flush时才会同步数据到数据库、执行校验。之前在测试方法内save非法实体后没有触发flush,非法实体一直留在缓存中,直到@AfterEach调用deleteAll()时JPA为了同步删除操作先flush缓存,才在销毁阶段抛出校验异常。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 16:09:21