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

如何用JUnit正确测试Spring的@NotBlank校验?异常未抛出求助

问题根源

MethodArgumentNotValidException是Spring MVC控制器层触发的异常:当控制器方法参数标注了@Valid,Spring会自动校验DTO的约束注解(如@NotBlank),校验失败时抛出该异常。而你直接调用ProductService的方法,Service层本身没有校验逻辑,自然不会触发这个异常。

解决方案

根据测试目标不同,有两种可行方案:

方案1:测试完整控制器请求流程(推荐)

使用Spring的MockMvc模拟HTTP请求,完整触发MVC层的校验逻辑,验证异常处理后的响应结果。

示例测试代码:

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ProductService productService;

    @Test
    void whenEmptyNameProduct_shouldReturnBadRequest() throws Exception {
        // 构造无效请求
        ProductRequest invalidRequest = new ProductRequest();
        invalidRequest.setAmount(0);
        invalidRequest.setPrice(500.0);

        // 模拟POST请求并断言响应
        mockMvc.perform(post("/product")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(invalidRequest)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.message").value("Name field is required"));
    }
}
  • @WebMvcTest仅加载控制器相关Bean,避免启动完整Spring上下文,测试更高效。
  • 通过MockMvc模拟请求,自动触发@Valid校验,最终验证全局异常处理器返回的错误响应。

方案2:单独测试DTO校验逻辑

如果只想验证ProductRequest的约束注解是否生效,可以直接使用Spring的校验器手动触发校验。

示例测试代码:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

class ProductRequestValidationTest {

    private final LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();

    @BeforeEach
    void setUp() {
        validator.afterPropertiesSet(); // 初始化校验器
    }

    @Test
    void whenEmptyName_shouldHaveValidationError() {
        ProductRequest request = new ProductRequest();
        request.setAmount(0);
        request.setPrice(500.0);

        // 手动校验DTO
        Set<ConstraintViolation<ProductRequest>> violations = validator.validate(request);
        
        // 断言校验结果
        assertFalse(violations.isEmpty());
        ConstraintViolation<ProductRequest> nameViolation = violations.stream()
                .filter(v -> v.getPropertyPath().toString().equals("name"))
                .findFirst()
                .orElseThrow();
        assertEquals("Name field is required", nameViolation.getMessage());
    }
}

可选:在Service层添加校验逻辑

如果希望Service层也能拦截无效参数,可以注入Validator手动触发校验,此时测试Service时会抛出ConstraintViolationException。

修改Service代码:

@Slf4j
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {

    private final ProductMapper productMapper;
    private final ProductRepository productRepository;
    private final Validator validator; // 注入Spring校验器

    @Override
    @Transactional
    public ProductResponse createProduct(ProductRequest productRequest) {
        // 手动触发校验
        Set<ConstraintViolation<ProductRequest>> violations = validator.validate(productRequest);
        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }

        Product product = productMapper.mapToEntity(productRequest);
        Product savedProduct = productRepository.save(product);
        log.info("New product saved. Id = {}", savedProduct.getId());
        return productMapper.mapToResponse(savedProduct);
    }
}

修改Service测试代码:

@Test
void whenEmptyNameProduct() {
    var productRequest = buildProduct();
    var product = productMapper.mapToEntity(productRequest);
    when(productRepository.save(any())).thenReturn(product);

    // 断言ConstraintViolationException
    ConstraintViolationException thrown = assertThrows(
            ConstraintViolationException.class,
            () -> productService.createProduct(productRequest));
    
    assertTrue(thrown.getConstraintViolations().stream()
            .anyMatch(v -> v.getMessage().equals("Name field is required")));
}

内容的提问来源于stack exchange,提问作者Luisian

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 11:14:55