更改 JUnit 测试顺序会中断使用来自每个 Hibernate Validator 的 PlatformResourceBundleLocator 的多个 JAR 文件的验证消息的测试

Changing JUnit test order breaks tests using validation messages from multiple JAR files per Hibernate Validator's PlatformResourceBundleLocator

我有一个 Gradle Spring Boot app running on Java 11 using Hibernate Validator. The app uses multiple custom library JARs with custom validation constraint annotations, each with its own ValidationMessages.properties files containing default messages for those annotations. This is supported using the built-in functionality in Hibernate's PlatformResourceBundleLocator 可以将多个 JAR 文件中的 ValidationMessages.properties 个文件聚合到一个包中:

@Configuration
public class ValidationConfig {
    @Bean
    public LocalValidatorFactoryBean validator() {
        PlatformResourceBundleLocator resourceBundleLocator =
                new PlatformResourceBundleLocator(ResourceBundleMessageInterpolator.USER_VALIDATION_MESSAGES, null, true);

        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setMessageInterpolator(new ResourceBundleMessageInterpolator(resourceBundleLocator));
        return factoryBean;
    }
}

jar1: ValidationMessages.properties

com.example.CustomValidation1.message=My custom message 1

jar2: ValidationMessages.properties

com.example.CustomValidation2.message=My custom message 2

项目中有相当多的单元和集成测试用于测试验证功能。其中一些测试在 Spring 聚合消息验证程序 bean 中自动装配。一些测试(特别是那些早于应用程序使用 multiple ValidationMessages.properties 的测试)不依赖于返回的确切消息,并且使用没有消息聚合的默认 Spring 验证器。虽然更新旧测试可能有意义,但为了优先级和可用时间,该任务已推迟到未来。

当我 运行 应用程序时,消息聚合功能按预期工作。当我 运行 在本地计算机上进行测试时,它也按预期工作。但是,当我的项目的测试通过 Jenkins 持续集成工具在构建服务器上 运行 时,一些验证测试失败。

我已根据 JUnit 测试 classes 运行 的顺序确定失败发生。测试在本地 运行 的顺序与在 Jenkins 上的顺序不同(这是允许的,因为 Gradle JUnit 插件不保证测试 class 执行顺序)。具体来说,任何测试是否失败取决于使用消息聚合验证器的测试是否先于使用消息聚合验证器的测试运行。

我已经能够在一次测试中将问题归结为一个简单的可重新创建的故障 class,如下所示。为了简化示例,@CustomValidation1 & @CustomValidation2 验证注释已编写为始终无法通过验证。

import com.example.CustomValidation1;
import com.example.CustomValidation2;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
import java.util.stream.Collectors;

import static org.junit.Assert.assertEquals;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class ValidationMessageTests {
    private Validator aggregateMessageValidator = createAggregateMessageValidator();

    private Validator standardValidator = createBasicValidator();

    private Validator createBasicValidator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.afterPropertiesSet();
        return factoryBean;
    }

    private Validator createAggregateMessageValidator() {
        PlatformResourceBundleLocator resourceBundleLocator =
                new PlatformResourceBundleLocator(ResourceBundleMessageInterpolator.USER_VALIDATION_MESSAGES, null, true);

        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setMessageInterpolator(new ResourceBundleMessageInterpolator(resourceBundleLocator));
        factoryBean.afterPropertiesSet();
        return factoryBean;
    }

    @Test
    public void test1() {
        Set<ConstraintViolation<MyInput>> violations = aggregateMessageValidator.validate(new MyInput());
        assertEquals(Set.of("My custom message 1", "My custom message 2"),
                violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.toSet()));
    }

    @Test
    public void test2() {
        Set<ConstraintViolation<MyInput>> violations = standardValidator.validate(new MyInput());
        assertEquals(2, violations.size());
    }

    @CustomValidation1
    @CustomValidation2
    private static class MyInput {
    }
}

test1test2 之前是 运行 时,两个测试都通过了。但是,当 test2 重命名为 test0 以便 test1 之前的 运行 时,test0 通过但 test1 失败并出现以下错误:

java.lang.AssertionError: 
Expected :[My custom message 1, My custom message 2]
Actual   :[My custom message 1, {com.example.CustomValidation2.message}]

为什么更改测试顺序会导致这些测试失败,我该如何解决?

代码当前正在使用 Spring Boot 2.2。4.RELEASE 和 Hibernate Validator 6.0。18.Final。

这个问题的根本原因是 ResourceBundles 在默认情况下被缓存。根据 ResourceBundle JavaDocs:

Resource bundle instances created by the getBundle factory methods are cached by default, and the factory methods return the same resource bundle instance multiple times if it has been cached.

由于此缓存,无论哪个测试首先导致 Hibernate Validator 加载 ValidationMessages.properties 文件,都将决定哪个 ResourceBundle 用于所有后续测试。当未使用自定义验证配置的测试首先是 运行 时,非聚合 [​​=24=]。当 PlatformResourceBundleLocator 加载资源包时,聚合逻辑将被忽略,因为已经缓存的 ResourceBundle 被使用。

修复很简单。 ResourceBundle 有一个 clearCache 方法来清除缓存,可以在 Hibernate Validator 检索包以创建验证消息之前调用该方法:

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class ValidationMessageTests {
    @Before
    public void setup() {
        ResourceBundle.clearCache();
    }

    // The rest of this test class is unchanged
}