Testing library for Spring Boot projects.
Add this library to the classpath by adding the following maven dependency. Versions can be found here
<dependency>
<groupId>it.aboutbits</groupId>
<artifactId>spring-boot-testing</artifactId>
<version>x.x.x</version>
</dependency>The validation tester allows us to quickly test simple validation constraints. Most commonly we use bean validation for this.
To use the validation tester in your project you need to extend both the BaseValidationAssert.java and the BaseRuleBuilder.java.
publicclassValidationAssertextendsBaseValidationAssert<BaseRuleBuilder<?>> {
protectedValidationAssert() {
super(newRuleBuilder());
}
publicstaticValidationAssertassertThatValidation() {
returnnewValidationAssert();
}
publicstaticfinalclassRuleBuilderextendsBaseRuleBuilder<RuleBuilder> {
}
}By default, the validation tester will assume that all properties of type Record are substructures. Therefore, using the @Valid annotation is required to make sure that validation for those records is triggered.
You can add a class to a whitelist to disable this behavior:
publicclassValidationConfig {
publicstaticvoidconfigure() {
ValidationAssert.registerNonBeanType(NotValidated.class);
}
}
publicclassValidationAssertextendsBaseValidationAssert<BaseRuleBuilder<?>> {
static {
ValidationConfig.configure();
}
// ...
}Each property is required to have at least one rule defined. You can add multiple rules for the same property as needed to combine more complex rulesets.
The tester will fail if not all properties have rules. In case you have properties without any restrictions, use the notValidated rule.
The validation tester works by taking in a valid parameter. It will then mutate the parameter internally and test each property with an invalid value. Then a check is done if a validation violation is raised as expected.
In any case, the call to isCompliant is required at the end and then triggers the actual assertion.
You can use plain bean validation to verify a Record:
importjakarta.validation.constraints.Future;
importjakarta.validation.constraints.NotNull;
importjakarta.validation.constraints.Past;
importorg.springframework.lang.Nullable;
publicrecordSomeParameter(
@NotBlankStringname,
@Min(18)
intage,
@NotNull@PastLocalDatebirthDay,
@NullableStringsomething,
StringnotValidatedAtAll
) {
}
@TestvoidtestValidation() {
varvalidParameter = newSomeParameter("Sepp", 32);
assertThatValidation().of(validParameter)
.usingBeanValidation()
.notBlank("name")
.min("age", 18)
.notNull("birthDay")
.past("birthDay")
.nullable("something")
.notValidated("notValidatedAtAll")
.isCompliant();
}Alternatively, you can use a method call to a service function to verify the validation. This is the preferred way as it makes sure that the bean validation is both triggered and also valid.
importorg.springframework.beans.factory.annotation.Autowired;
publicrecordSomeParameter(
@NotBlankStringname,
@Min(18)
intage
) {
}
@AutowiredprivateMyServicemyService;
@TestvoidtestValidation() {
varvalidParameter = newSomeParameter("Sepp", 32);
assertThatValidation().of(validParameter)
.calling(myService::create)
.notBlank("name")
.min("age", 18)
.isCompliant();
}
@TestvoidtestValidationWithIdParameter() {
varvalidParameter = newSomeParameter("Sepp", 32);
assertThatValidation().of(validParameter)
.calling(myService::update, newUser.ID(3L))
.notBlank("name")
.min("age", 18)
.isCompliant();
}You can add new rules by creating a new interface:
publicinterfaceMyShinyNewRule<VextendsBaseRuleBuilder<?>> extendsValidationRulesData {
defaultVshiny(@NonNullStringproperty) {
this.addRule(newRule(property, InertValueSource.class, newObject[0]));
return (BaseRuleBuilder) this;
}
}To use the newly created rule, we can simply have our RuleBuilder implement the interface:
publicclassValidationAssertextendsBaseValidationAssert<BaseRuleBuilder<?>> {
// ...publicstaticfinalclassRuleBuilderextendsBaseRuleBuilder<RuleBuilder> implementsMyShinyNewRule<RuleBuilder> {
}
}The Rule requires the property name, a value-source and an array of optional parameters. For example min(property, minValue) takes in the additional parameter for the value.
Note that the value-source must return invalid values. This is required because the tool is actively trying to violate the rules to check if an error is raised.
You can add custom values sources by implementing the ValueSource interface.
While the interface can not enforce the static function registerType, it is best practice to implement it in a way that keeps this extensible.
This way we can use the same logical value-source for multiple property types.
Here is an example:
publicclassEmptyValueSourceimplementsValueSource {
privatestaticfinalMap<Class<?>, Function<Object[], Stream<?>>> TYPE_SOURCES = newHashMap<>();
static {
TYPE_SOURCES.put(
String.class,
(Object[] args) -> Stream.of("")
);
TYPE_SOURCES.put(
Set.class,
(Object[] args) -> Stream.of(newHashSet<>())
);
TYPE_SOURCES.put(
List.class,
(Object[] args) -> Stream.of(newArrayList<>())
);
}
publicstaticvoidregisterType(Class<?> type, Function<Object[], Stream<?>> source) {
TYPE_SOURCES.put(type, source);
}
@Override@SuppressWarnings("unchecked")
public <T> Stream<T> values(Class<T> propertyClass, Object... args) {
varsourceFunction = TYPE_SOURCES.get(propertyClass);
if (sourceFunction != null) {
return (Stream<T>) sourceFunction.apply(args);
}
thrownewIllegalArgumentException("Property class not supported!");
}
}Note: CustomType wrappers from the toolbox are currently not natively supported.
Adding custom types will require some extension to the existing value-sources. Those need to become aware of the new type in order to produce values of said type.
This can be done by extending the configuration:
publicrecordSurnameType(
Stringvalue
) {
}
publicclassValidationConfig {
publicstaticvoidconfigure() {
EmptyValueSource.registerType(
SurnameType.class,
(args) -> {
returnStream.of(newSurnameType(""));
}
);
// ...
}
}
publicclassValidationAssertextendsBaseValidationAssert<BaseRuleBuilder<?>> {
static {
ValidationConfig.configure();
}
// ...
}To use this library as a local development dependency, you can simply refer to the version BUILD-SNAPSHOT.
Check out this repository and run the maven goal install. This will build and install this library as version BUILD-SNAPSHOT into your local maven cache.
Note that you may have to tell your IDE to reload your main maven project each time you build the library.
To build and publish the chart, visit the GitHub Actions page of the repository and trigger the workflow "Release Package" manually.
About Bits is a company based in South Tyrol, Italy. You can find more information about us on our website.
For support, please contact info@aboutbits.it.
The MIT License (MIT). Please see the license file for more information.