Skip to content

Commit c90e85e

Browse files
authored
Merge pull request #2 from aboutbits/aic-1195-be-improve-validation-tests
Aic 1195 be improve validation tests
2 parents cdc5d90 + 92273f7 commit c90e85e

36 files changed

Lines changed: 2035 additions & 3 deletions

.github/workflows/publish.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ jobs:
1818
steps:
1919
- uses: actions/checkout@v4
2020

21-
- uses: aboutbits/github-actions-java/setup-and-install@v3
21+
- uses: aboutbits/github-actions-java/setup@v3
2222

2323
- name: Set Version
2424
run: sed -i 's|<version>BUILD-SNAPSHOT</version>|<version>${{ github.event.inputs.version }}</version>|g' pom.xml
2525

2626
- name: Publish package
27-
run: mvn --batch-mode deploy
27+
run: mvn -s $GITHUB_WORKSPACE/.github/workflows/maven-settings.xml --batch-mode deploy
2828
env:
29-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
GITHUB_USER_NAME: ${{ github.actor }}
30+
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3031

3132
tag:
3233
timeout-minutes: 5

.idea/encodings.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pom.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
44
<modelVersion>4.0.0</modelVersion>
55

6+
<parent>
7+
<groupId>org.springframework.boot</groupId>
8+
<artifactId>spring-boot-starter-parent</artifactId>
9+
<version>3.3.2</version>
10+
<relativePath/> <!-- lookup parent from repository -->
11+
</parent>
12+
613
<groupId>it.aboutbits</groupId>
714
<artifactId>spring-boot-testing</artifactId>
815
<version>BUILD-SNAPSHOT</version>
@@ -13,7 +20,29 @@
1320
</properties>
1421

1522
<dependencies>
23+
<dependency>
24+
<groupId>it.aboutbits</groupId>
25+
<artifactId>spring-boot-toolbox</artifactId>
26+
<version>1.0.0-RC1</version>
27+
</dependency>
28+
29+
<dependency>
30+
<groupId>org.springframework.boot</groupId>
31+
<artifactId>spring-boot-starter-validation</artifactId>
32+
</dependency>
33+
34+
<!-- Utilities -->
35+
<dependency>
36+
<groupId>org.projectlombok</groupId>
37+
<artifactId>lombok</artifactId>
38+
<optional>true</optional>
39+
</dependency>
1640

41+
<!-- Testing -->
42+
<dependency>
43+
<groupId>org.springframework.boot</groupId>
44+
<artifactId>spring-boot-starter-test</artifactId>
45+
</dependency>
1746
</dependencies>
1847

1948
<build>

readme.md

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,256 @@ Testing library for Spring Boot projects.
77
Add this library to the classpath by adding the following maven dependency. Versions can be found [here](../../packages)
88

99
```xml
10+
1011
<dependency>
1112
<groupId>it.aboutbits</groupId>
1213
<artifactId>spring-boot-testing</artifactId>
1314
<version>x.x.x</version>
1415
</dependency>
1516
```
1617

18+
## Usage
19+
20+
### Validation
21+
22+
The validation tester allows us to quickly test simple validation constraints. Most commonly we use bean validation for this.
23+
24+
#### Configuration
25+
26+
To use the validation tester in your project you need to extend both the [BaseValidationAssert.java](src/main/java/it/aboutbits/springboot/testing/validation/core/BaseValidationAssert.java) and the [BaseRuleBuilder.java](src/main/java/it/aboutbits/springboot/testing/validation/core/BaseRuleBuilder.java).
27+
28+
```java
29+
public class ValidationAssert extends BaseValidationAssert<BaseRuleBuilder<?>> {
30+
protected ValidationAssert() {
31+
super(new RuleBuilder());
32+
}
33+
34+
public static ValidationAssert assertThatValidation() {
35+
return new ValidationAssert();
36+
}
37+
38+
public static final class RuleBuilder extends BaseRuleBuilder<RuleBuilder> {
39+
40+
}
41+
}
42+
```
43+
44+
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.
45+
You can add a class to a whitelist to disable this behavior:
46+
47+
```java
48+
49+
public class ValidationConfig {
50+
public static void configure() {
51+
ValidationAssert.registerNonBeanType(NotValidated.class);
52+
}
53+
}
54+
55+
public class ValidationAssert extends BaseValidationAssert<BaseRuleBuilder<?>> {
56+
static {
57+
ValidationConfig.configure();
58+
}
59+
60+
// ...
61+
}
62+
```
63+
64+
#### Usage
65+
66+
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.
67+
The tester will fail if not all properties have rules. In case you have properties without any restrictions, use the `notValidated` rule.
68+
69+
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.
70+
71+
In any case, the call to `isCompliant` is required at the end and then triggers the actual assertion.
72+
73+
You can use plain bean validation to verify a Record:
74+
75+
```java
76+
import jakarta.validation.constraints.Future;
77+
import jakarta.validation.constraints.NotNull;
78+
import jakarta.validation.constraints.Past;
79+
import org.springframework.lang.Nullable;
80+
81+
public record SomeParameter(
82+
@NotBlank
83+
String name,
84+
@Min(18)
85+
int age,
86+
@NotNull
87+
@Past
88+
LocalDate birthDay,
89+
@Nullable
90+
String something,
91+
String notValidatedAtAll
92+
) {
93+
}
94+
95+
96+
@Test
97+
void testValidation() {
98+
var validParameter = new SomeParameter("Sepp", 32);
99+
100+
assertThatValidation().of(validParameter)
101+
.usingBeanValidation()
102+
.notBlank("name")
103+
.min("age", 18)
104+
.notNull("birthDay")
105+
.past("birthDay")
106+
.nullable("something")
107+
.notValidated("notValidatedAtAll")
108+
.isCompliant();
109+
}
110+
```
111+
112+
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.
113+
114+
```java
115+
import org.springframework.beans.factory.annotation.Autowired;
116+
117+
public record SomeParameter(
118+
@NotBlank
119+
String name,
120+
@Min(18)
121+
int age
122+
) {
123+
}
124+
125+
@Autowired
126+
private MyService myService;
127+
128+
129+
@Test
130+
void testValidation() {
131+
var validParameter = new SomeParameter("Sepp", 32);
132+
133+
assertThatValidation().of(validParameter)
134+
.calling(myService::create)
135+
.notBlank("name")
136+
.min("age", 18)
137+
.isCompliant();
138+
}
139+
140+
@Test
141+
void testValidationWithIdParameter() {
142+
var validParameter = new SomeParameter("Sepp", 32);
143+
144+
assertThatValidation().of(validParameter)
145+
.calling(myService::update, new User.ID(3L))
146+
.notBlank("name")
147+
.min("age", 18)
148+
.isCompliant();
149+
}
150+
```
151+
152+
#### Adding custom validation rules
153+
154+
You can add new rules by creating a new interface:
155+
156+
```java
157+
public interface MyShinyNewRule<V extends BaseRuleBuilder<?>> extends ValidationRulesData {
158+
default V shiny(@NonNull String property) {
159+
this.addRule(new Rule(property, InertValueSource.class, new Object[0]));
160+
return (BaseRuleBuilder) this;
161+
}
162+
}
163+
```
164+
165+
To use the newly created rule, we can simply have our `RuleBuilder` implement the interface:
166+
167+
```java
168+
public class ValidationAssert extends BaseValidationAssert<BaseRuleBuilder<?>> {
169+
// ...
170+
171+
public static final class RuleBuilder extends BaseRuleBuilder<RuleBuilder> implements MyShinyNewRule<RuleBuilder> {
172+
173+
}
174+
}
175+
```
176+
177+
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.
178+
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.
179+
180+
#### Adding custom value sources
181+
182+
You can add custom values sources by implementing the `ValueSource` interface.
183+
While the interface can not enforce the static function `registerType`, it is best practice to implement it in a way that keeps this extensible.
184+
This way we can use the same logical value-source for multiple property types.
185+
186+
Here is an example:
187+
188+
```java
189+
public class EmptyValueSource implements ValueSource {
190+
private static final Map<Class<?>, Function<Object[], Stream<?>>> TYPE_SOURCES = new HashMap<>();
191+
192+
static {
193+
TYPE_SOURCES.put(
194+
String.class,
195+
(Object[] args) -> Stream.of("")
196+
);
197+
TYPE_SOURCES.put(
198+
Set.class,
199+
(Object[] args) -> Stream.of(new HashSet<>())
200+
);
201+
TYPE_SOURCES.put(
202+
List.class,
203+
(Object[] args) -> Stream.of(new ArrayList<>())
204+
);
205+
}
206+
207+
public static void registerType(Class<?> type, Function<Object[], Stream<?>> source) {
208+
TYPE_SOURCES.put(type, source);
209+
}
210+
211+
@Override
212+
@SuppressWarnings("unchecked")
213+
public <T> Stream<T> values(Class<T> propertyClass, Object... args) {
214+
var sourceFunction = TYPE_SOURCES.get(propertyClass);
215+
if (sourceFunction != null) {
216+
return (Stream<T>) sourceFunction.apply(args);
217+
}
218+
219+
throw new IllegalArgumentException("Property class not supported!");
220+
}
221+
}
222+
```
223+
224+
#### Adding support for custom types
225+
226+
_Note: CustomType wrappers from the `toolbox` are currently not natively supported._
227+
228+
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.
229+
230+
This can be done by extending the configuration:
231+
232+
```java
233+
public record SurnameType(
234+
String value
235+
) {
236+
}
237+
238+
public class ValidationConfig {
239+
public static void configure() {
240+
EmptyValueSource.registerType(
241+
SurnameType.class,
242+
(args) -> {
243+
return Stream.of(new SurnameType(""));
244+
}
245+
);
246+
247+
// ...
248+
}
249+
}
250+
251+
public class ValidationAssert extends BaseValidationAssert<BaseRuleBuilder<?>> {
252+
static {
253+
ValidationConfig.configure();
254+
}
255+
256+
// ...
257+
}
258+
```
259+
17260
## Local development:
18261

19262
To use this library as a local development dependency, you can simply refer to the version `BUILD-SNAPSHOT`.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package it.aboutbits.springboot.testing.validation.core;
2+
3+
import it.aboutbits.springboot.testing.validation.rule.BetweenRule;
4+
import it.aboutbits.springboot.testing.validation.rule.FutureRule;
5+
import it.aboutbits.springboot.testing.validation.rule.MaxRule;
6+
import it.aboutbits.springboot.testing.validation.rule.MinRule;
7+
import it.aboutbits.springboot.testing.validation.rule.NegativeOrZeroRule;
8+
import it.aboutbits.springboot.testing.validation.rule.NegativeRule;
9+
import it.aboutbits.springboot.testing.validation.rule.NotBlankRule;
10+
import it.aboutbits.springboot.testing.validation.rule.NotEmptyRule;
11+
import it.aboutbits.springboot.testing.validation.rule.NotNullRule;
12+
import it.aboutbits.springboot.testing.validation.rule.NotValidatedRule;
13+
import it.aboutbits.springboot.testing.validation.rule.NullableRule;
14+
import it.aboutbits.springboot.testing.validation.rule.PastRule;
15+
import it.aboutbits.springboot.testing.validation.rule.PositiveOrZeroRule;
16+
import it.aboutbits.springboot.testing.validation.rule.PositiveRule;
17+
import it.aboutbits.springboot.testing.validation.rule.ValidBeanRule;
18+
import lombok.AccessLevel;
19+
import lombok.Getter;
20+
import lombok.NonNull;
21+
import lombok.RequiredArgsConstructor;
22+
import lombok.Setter;
23+
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
27+
@RequiredArgsConstructor
28+
public abstract class BaseRuleBuilder<R extends BaseRuleBuilder<?>> implements
29+
ValidationRulesData,
30+
BetweenRule<R>,
31+
FutureRule<R>,
32+
MaxRule<R>,
33+
MinRule<R>,
34+
NegativeOrZeroRule<R>,
35+
NegativeRule<R>,
36+
NotBlankRule<R>,
37+
NotEmptyRule<R>,
38+
NotNullRule<R>,
39+
NullableRule<R>,
40+
PastRule<R>,
41+
PositiveOrZeroRule<R>,
42+
PositiveRule<R>,
43+
NotValidatedRule<R>,
44+
ValidBeanRule<R> {
45+
@Getter(AccessLevel.PACKAGE)
46+
private final List<Rule> rules = new ArrayList<>();
47+
48+
@Setter(AccessLevel.PACKAGE)
49+
private Runnable triggerValidation;
50+
51+
@Override
52+
public void addRule(@NonNull Rule rule) {
53+
rules.add(rule);
54+
}
55+
56+
public void isCompliant() {
57+
triggerValidation.run();
58+
}
59+
}

0 commit comments

Comments
 (0)