Spring-Boot 单元测试:ConstraintValidator 中的@Value

Posted

技术标签:

【中文标题】Spring-Boot 单元测试:ConstraintValidator 中的@Value【英文标题】:Spring-Boot UnitTest: @Value in ConstraintValidator 【发布时间】:2019-10-19 04:34:58 【问题描述】:

我目前正在提供覆盖 - 通过 MockMVC 请求调用测试我的 DTO 的验证。 我最近在我的注册约束验证器中引入了一个新字段,supportedSpecializations,我从 application.properties 中注入了值,以便于维护和扩展。请参阅下面的代码片段:

@Component
public class RegistrationValidator implements ConstraintValidator<Registration, String> 

    //campus.students.supportedspecializations="J2E,.NET,OracleDB,mysql,Angular"

    @Value("$campus.students.supportedspecializations")
    private String supportedSpecializations;

    private String specializationExceptionMessage;

    //All ExceptionMessages are maintained in a separate class
    @Override
    public void initialize(Registration constraintAnnotation) 
        exceptionMessage = constraintAnnotation.regionException().getMessage();
    

    @Override
    public boolean isValid(RegistrationData regData, ConstraintValidatorContext context) 

        String[] specializations = supportedSpecializations.split(",");
        boolean isValidSpecialization = Arrays.stream(specializations)
                    .anyMatch(spec -> spec.equalsIgnoreCase(regData.getSpec()));
        if (!isValidSpecialization)
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(specializationExceptionMessage)
                        .addConstraintViolation();
            return false;
            
        //additional validation logic...
        return true;
    

单元测试现在由于@Value 注释的定义属性未注入该字段而失败。 我不确定 ReflectionTestUtils 是否对我的情况有所帮助,因此非常感谢任何有关如何在 UnitTests 中注入所需值的建议。


Spring 版本是 2.1.0 我目前正在使用以下 sn-p 进行测试:

@InjectMocks
private StudentController mockRestController;

@Mock
private StudentService mockStudentService;

@Mock
private ValidationExceptionTranslator mockExceptionTranslator;

@Value("$campus.students.supportedspecializations")
private String supportedSpecializations;

private MockMvc mockMvc;

private static final String VALIDATION_SUCCESSFUL = "success";
private static final String VALIDATION_FAILED = "failed";

@Before
public void setup() 
    MockitoAnnotations.initMocks(this);
    this.mockMvc = MockMvcBuilders.standaloneSetup(mockRestController).build();

    doReturn(
            ResponseEntity.status(HttpStatus.OK)
            .header("Content-Type", "text/html; charset=utf-8")
            .body(VALIDATION_SUCCESSFUL))
    .when(mockStudentService).insertStudent(Mockito.any());

    doReturn(
            ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .header("Content-Type", "application/json")
            .body(VALIDATION_FAILED))
    .when(mockExceptionTranslator).translate(Mockito.any());


@Test
public void testValidation_UnsupportedSpecialization() throws Exception 

    MvcResult mvcResult = mockMvc.perform(
            post("/Students").contentType(MediaType.APPLICATION_JSON_UTF8).content(
                    "\"registrationData\":\"spec\":\"unsupported\""))
            .andExpect(status().isBadRequest())
            .andReturn();

    assertEquals(VALIDATION_FAILED, mvcResult.getResponse().getContentAsString());

    verify(mockExceptionTranslator, times(1)).translate(Mockito.any());
    verify(mockStudentService, times(0)).insertStudent(Mockito.any());


我尝试使用 @RunWith(SpringRunner.class)@SpringBootTest(classes = Application.class) 注释我的测试类,但验证测试仍然失败,原因是@Value 未解决。我可能错了,但我认为 ConstraintValidator 的实例是在我们到达 restController 之前创建的,所以 MockMVC perform(...) 调用不能简单地确保验证器中的适当 @Value 得到注入supportedSpecializations。

【问题讨论】:

你能发布你的单元测试吗?只是为了确定,但似乎您没有加载弹簧上下文,因此@Value 中没有注入任何值。 @XavierBouclet 我更新了帖子,请看一下 你在嘲笑你的控制器并嘲笑很多东西。默认情况下,Spring 不为处理约束验证器做任何事情。您将需要一个适当的@WebMvcTest 或完整的@SpringBootTest 测试,否则@Value 将无法解决。紧接着 Spring 并没有真正控制创建这些验证器的实例,而是验证器实现(在本例中为休眠),并且根据版本,Spirng 甚至不会处理该类。 【参考方案1】:

通过以下方式解决了该问题: 在测试类中添加了以下注释

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc

然后自动装配 controllermockMVC,最后用 Spring 的 @MockBean

注释服务和翻译器

所以目前它看起来像这样:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc
public class StudentValidatorTest 

    @Autowired
    private StudentController mockRestController;

    @MockBean
    private StudentService mockStudentService;

    @MockBean
    private ValidationExceptionTranslator mockExceptionTranslator;

    @Autowired
    private MockMvc mockMvc;

    private static final String VALIDATION_SUCCESSFUL = "success";
    private static final String VALIDATION_FAILED = "failed";

    @Before
    public void setup() 
        MockitoAnnotations.initMocks(this);
        this.mockMvc = MockMvcBuilders.standaloneSetup(mockRestController).build();

        doReturn(
            ResponseEntity.status(HttpStatus.OK)
            .header("Content-Type", "text/html; charset=utf-8")
            .body(VALIDATION_SUCCESSFUL))
        .when(mockStudentService).insertStudent(Mockito.any());

        doReturn(
                ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .header("Content-Type", "application/json")
                .body(VALIDATION_FAILED))
        .when(mockExceptionTranslator).translate(Mockito.any());
    

//...and tests...

【讨论】:

【参考方案2】:

是的, 使用ReflectionTestUtil

使用ReflectionTestUtil.setField设置supportedSpecializations的值 setup() 方法(junit @Before 注释方法(junit > 1.4)中。

更多详情 我建议不要使用MockMVC 进行单元测试; 集成测试很好, 只是不是单元测试。

单元测试不需要启动 Spring; 您永远不需要 Spring 为您的单元测试执行注入。 反而, 实例化您正在测试的类并直接调用方法。

这是一个简单的例子:

public class TestRegistrationValidator

  private static final String VALUE_EXCEPTION_MESSAGE = "VALUE_EXCEPTION_MESSAGE";
    private static final String VALUE_SUPPORTED_SPECIALIZATIONS = "BLAMMY,KAPOW";

    private RegistrationValidator classToTest;

    @Mock
    private Registration mockRegistration;

    @Mock
    private RegionExceptionType mockRegionExceptionType; // use the actual type of regionExcpeption.

    @Before
    public void preTestSetup()
    
        MockitoAnnotations.initMocks(this);

        ReflectionTestUtils.setField(classToTest, "supportedSpecializations", VALUE_SUPPORTED_SPECIALIZATIONS);

        doReturn(VALUE_EXCEPTION_MESSAGE).when(mockRegionExceptionType).getMessage();

        doReturn(mockRegionExceptionType).when(mockRegion).regionException();
    

    @Test
    public void initialize_allGood_success()
    
        classToTest.initialize(mockRegistration);

        ...assert some stuff.
        ...perhaps verify some stuff.
    

【讨论】:

您能简单描述一下如何做到这一点吗?根据 Baeldung 的自定义约束教程,我通过 MockMVC 执行方法调用该功能。有没有办法通过 MockMVC / Mocked RestController 提取和模拟某些 constraintValidator?【参考方案3】:

我认为对您来说最好的选择是在您的 RegistrationValidator.class 中使用构造函数注入,这样您就可以在需要时直接分配模拟或测试值进行测试。示例:

@Component
class ExampleClass 

    final String text

    // Use @Autowired to get @Value to work.
    @Autowired
    ExampleClass(
        // Refer to configuration property
        // app.message.text to set value for 
        // constructor argument message.
        @Value('$app.message.text') final String text) 
        this.text = text
    


通过这种方式,您可以将模拟值设置为用于单元测试的变量。 是的,你是对的,在这里自定义构造函数不是一个选项,然后你可以引入一个配置类,你可以在其中从 yml 或属性中读取这些值并在验证器中自动装配。这应该适合你。

或者

您可以在单独的test.ymltest.properties 中提供@Value 属性,并指定在运行集成测试时使用该属性。在这种情况下,您应该能够解析这些值。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ExampleApplication.class)
@TestPropertySource(locations="classpath:test.properties")
public class ExampleApplicationTests 


@TestPropertySource 注释具有更高的优先顺序,它应该解析您的值。

【讨论】:

不幸的是,两者都没有帮助。我用细节更新了这个问题。问题是,我无法为 ConstraintValidator 编写自定义构造函数,并且第二种解决方案不适用于较新的 Spring-Boot 应用程序。我使用 2.1.0 是的,你是对的,然后你可以引入一个配置类,你可以在其中从 yml 或属性中读取这些值并在验证器中自动装配。这应该适合你。

以上是关于Spring-Boot 单元测试:ConstraintValidator 中的@Value的主要内容,如果未能解决你的问题,请参考以下文章

spring-boot2.0 单元测试JUnit4

Spring-Boot 单元测试:ConstraintValidator 中的@Value

spring-boot 速成(11) - 单元测试

spring-boot-单元测试

基于spring-boot的应用程序的单元测试方案

spring-boot学习写一个简单的单元测试