Skip to content

Commit 1c7a1bb

Browse files
committed
add documentation and extract some methods
1 parent 188f27a commit 1c7a1bb

4 files changed

Lines changed: 315 additions & 6 deletions

File tree

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`.

src/main/java/it/aboutbits/springboot/testing/validation/core/BaseValidationAssert.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public abstract class BaseValidationAssert<R extends BaseRuleBuilder<?>> {
1717
@Getter(AccessLevel.PROTECTED)
1818
private final R ruleBuilder;
1919

20+
// This keeps track of classes that are not required to have a @Valid annotation.
2021
protected static final Set<Class<?>> NON_BEAN_TYPES = new HashSet<>(
2122
Set.of(
2223
CustomType.class
@@ -28,6 +29,11 @@ public abstract class BaseValidationAssert<R extends BaseRuleBuilder<?>> {
2829
@Setter(AccessLevel.PRIVATE)
2930
private Consumer<?> functionToCallWithParameter = null;
3031

32+
/**
33+
* Configure a class that is not required to have a @Valid annotation. Sub-structures are assumed to always require @Valid.
34+
*
35+
* @param type The class to whitelist.
36+
*/
3137
public static void registerNonBeanType(Class<?> type) {
3238
NON_BEAN_TYPES.add(type);
3339
}

0 commit comments

Comments
 (0)