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

huangapple go评论78阅读模式
英文:

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

问题

我有一个运行在Java 11上的Gradle Spring Boot 应用,使用Hibernate Validator。该应用使用多个自定义库JAR,其中包含自定义验证约束注解,每个JAR都有自己的ValidationMessages.properties文件,其中包含这些注解的默认消息。这是通过Hibernate的PlatformResourceBundleLocator内置功能来支持的,它可以将来自多个JAR文件的ValidationMessages.properties文件聚合到一个单一的bundle中:

@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。一些测试(特别是在应用程序使用多个ValidationMessages.properties之前的测试)不依赖于返回的确切消息,而是使用默认的Spring验证器,而不进行消息聚合。虽然更新旧测试可能有道理,但出于优先级和可用时间的考虑,这项任务已被推迟到将来。

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

我已经确定了失败是基于Junit测试类的运行顺序而发生的。本地运行测试时,测试以不同的顺序运行,而在Jenkins上运行测试则以不同的顺序运行(这是允许的,因为Gradle JUnit插件不保证测试类的执行顺序)。具体来说,任何测试是否失败都取决于一个使用消息聚合验证器的测试是否在使用不进行消息聚合的验证器之前运行。

我已经能够将问题归结为一个简单的可再现故障,如下所示。为了简单起见,@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 {
    }
}

当将test1放在test2之前运行时,两个测试都通过。但是,当将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。

英文:

I have a 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 to aggregate ValidationMessages.properties files from multiple JAR files into a single bundle:

@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

There are a fair number of unit & integration tests in the project which test the validation functionality. Some of these tests autowire in the Spring aggregated message validator bean. Some of the tests (in particular ones which predate the application's usage of multiple ValidationMessages.properties) do not depend on the exact messages returned, and use the default Spring validator without the message aggregation. While it may make sense to update the older tests, in the interest of priorities and available time that task has been deferred to the future.

The message aggregation functionality is working as expected when I run the application. It is also working as expected when I run the tests on my local machine. However, when my project's tests are run on the build server via the Jenkins continuous integration tool, some of the validation tests fail.

I have determined the failure occurs based on the order in which the JUnit test classes are run. The tests are being run in a different order locally than they are on Jenkins (which is allowed since the Gradle JUnit plugin does not guarantee test class execution order). Specifically, whether any tests fail vary based on whether or not a test which uses a message aggregation validator runs before one that uses a validator without message aggregation.

I have been able to boil down the issue to a simple re-creatable failure within a single test class, as follows. In the interest of example simplicity, the @CustomValidation1 & @CustomValidation2 validation annotations have been written to always fail validation.

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&lt;ConstraintViolation&lt;MyInput&gt;&gt; violations = aggregateMessageValidator.validate(new MyInput());
        assertEquals(Set.of(&quot;My custom message 1&quot;, &quot;My custom message 2&quot;),
                violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.toSet()));
    }

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

    @CustomValidation1
    @CustomValidation2
    private static class MyInput {
    }
}

When test1 is run before test2, both tests pass. However, when test2 is renamed to test0 so that the it runs before test1, test0 passes but test1 fails with the following error:

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

Why does changing the test order cause these tests to fail, and how do I fix it?

The code is currently using Spring Boot 2.2.4.RELEASE with Hibernate Validator 6.0.18.Final.

答案1

得分: 0

以下是翻译好的内容:

问题的根本原因是ResourceBundle默认会被缓存。根据ResourceBundle JavaDocs

> 由getBundle工厂方法创建的资源包实例默认会被缓存,如果已经被缓存,工厂方法会多次返回同一个资源包实例。

由于这种缓存机制,无论哪个测试先导致 Hibernate Validator 加载 ValidationMessages.properties 文件,都将决定所有后续测试使用哪个 ResourceBundle。当首先运行一个不使用自定义验证配置的测试时,会为 ValidationMessages 缓存一个非聚合的 PropertyResourceBundle,而不是所需的 AggregateResourceBundle。当 PlatformResourceBundleLocator 加载资源包时,聚合逻辑会被忽略,因为已经缓存的 ResourceBundle 被直接使用。

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

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

    // 测试类的其余部分保持不变
}
英文:

This underlying cause of this issue is that ResourceBundles are cached by default. Per the 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.

Due to this caching, whichever test first causes Hibernate Validator to load the ValidationMessages.properties file(s) will determine which ResourceBundle is used for all subsequent tests. When a test that is not using the customized validation configuration is run first, a non-aggregated PropertyResourceBundle will be cached for ValidationMessages, rather than the desired AggregateResourceBundle. When the PlatformResourceBundleLocator loads the resource bundle, the aggregation logic is ignored since the already cached ResourceBundle is used instead.

The fix is straightforward. ResourceBundle has a clearCache method to clear the cache, which can be called before Hibernate Validator would retrieve the bundle to create a validation message:

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

    // The rest of this test class is unchanged
}

huangapple
  • 本文由 发表于 2020年5月2日 15:16:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/61555747.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定