英文:
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<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 {
}
}
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 ResourceBundle
s 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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论