Search code examples
springspring-bootspring-testspring-boot-test

How to use Spring defaultValidator in JUnit tests


I have been using @SpringJunitConfig on my test classes to reduce the context load time over @SpringBootTest. This has worked well when I was using only my own classes as I can easily specify the packages / classes to be loaded.

Now I'm trying to use Spring's default argument validation. Based on other SO answers I have created and loaded defaultValidator bean. However Spring's default validation is not triggered when my tests call a method with validations and the test fails. I know the annotations on the class under test are correct because when I switch to @SpringBootTest the test passes.

Any additional ideas?

This is the closest I've come but no automatic validation by Spring occurs, unless I switch to @SpringBootTest which loads the full context and is too slow.

Test Class

@SpringJUnitConfig()
class UserServiceImplTest {
    @Configuration
    @ComponentScan(basePackages = { "com.user" },
            includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = { UserMapper.class,
                    UserServiceImpl.class}), useDefaultFilters = false)
    static class ConfigMe {
        @Bean
        public BCryptPasswordEncoder bCryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }

        @Bean
        public Validator defaultValidator() {
            ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
            return factory.getValidator();
        }
    }

    @Autowired
    private UserServiceImpl userService;

    @Autowired
    private UserMapper mapper;

    @Autowired
    @Qualifier("defaultValidator")
    Validator validator;

    @MockBean
    private UserDao userDao;

    // tests for UserServiceImpl that require valdiation of method arguments
    // e.g. public UserDto findUser(@NotNull @Size(min = 30, max = 30) String userUnique)

    @Test
    void givenInvalidUnique_whenFind_thenConstraintException() {
        assertThrows(ConstraintViolationException.class, () -> {
            userService.findUser(null); // null
        });
    }

Service Class

@Service
@Transactional
@Validated
public class UserServiceImpl implements UserService {

    @Override
    @Transactional(readOnly = true)
    public UserDto findUser(@NotNull @Size(min = 30, max = 30) String userUnique) {
        log.trace("findUser called with unique [{}]", userUnique);
        Optional<User> foundUser = userDao.findByUserUnique(userUnique);
        if (foundUser.isEmpty())
            throw new MyEntityNotFoundException(String.format("Could not find user with unique of [%s]", userUnique));

        return mapper.UserEntityToDto(foundUser.get());
    }
    
    // other service methods
}

Solution

  • The defaultValidator bean is defined by the spring-boot auto-configuration stuff which will be enabled if there are any @Configuration bean annotated with @EnableAutoConfiguration.

    But @EnableAutoConfiguration by default will consider all the auto-configuration stuff defined by spring-boot (i.e. those defined in spring.factories) which may be too much for writing a unit test. So there are another annotation called @ImportAutoConfiguration which can just import and apply a specified auto-configuration class.

    As the defaultValidator bean is defined in the ValidationAutoConfiguration , that means you can simply import it by :

    @SpringJUnitConfig
    class UserServiceImplTest {
    
        @Configuration
        @ComponentScan(basePackages = { "com.user" }, includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = { UserMapper.class, UserServiceImpl.class}), useDefaultFilters = false)
        @ImportAutoConfiguration(ValidationAutoConfiguration.class)                
        static class ConfigMe {
            @Bean
            public BCryptPasswordEncoder bCryptPasswordEncoder() {
                return new BCryptPasswordEncoder();
            }
        }
    
     }
    

    Tips : To find out which auto-configuration class defines defaultValidator bean , you can enable debug mode in application.properties :

    debug=true
    

    then start the spring-boot application as usual and it will print out the following conditions evaluation report :

    ============================
    CONDITIONS EVALUATION REPORT
    ============================
    
    Positive matches:
    -----------------
    
       ValidationAutoConfiguration matched:
          - @ConditionalOnClass found required class 'javax.validation.executable.ExecutableValidator' (OnClassCondition)
          - @ConditionalOnResource found location classpath:META-INF/services/javax.validation.spi.ValidationProvider (OnResourceCondition)
    
       ValidationAutoConfiguration#defaultValidator matched:
          - @ConditionalOnMissingBean (types: javax.validation.Validator; SearchStrategy: all) did not find any beans (OnBeanCondition)