Skip to content

Commit 8f93aed

Browse files
committed
Add Junit custom annotation to validate record mandatory fields
1 parent 46d2375 commit 8f93aed

File tree

6 files changed

+164
-2
lines changed

6 files changed

+164
-2
lines changed

boot-examples/pom.xml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<openapi-generator-maven-plugin.version>7.0.1</openapi-generator-maven-plugin.version>
2424
<wiremock-jre8.version>3.0.1</wiremock-jre8.version>
2525
<awaitility.version>4.2.1</awaitility.version>
26+
<instancio-junit.version>5.3.0</instancio-junit.version>
27+
<spring-boot-testjars.version>0.0.1</spring-boot-testjars.version>
2628
</properties>
2729
<dependencies>
2830
<dependency>
@@ -96,15 +98,22 @@
9698
<dependency>
9799
<groupId>org.springframework.boot</groupId>
98100
<artifactId>spring-boot-dependencies</artifactId>
99-
<version>3.3.1</version>
101+
<version>3.4.2</version>
100102
<type>pom</type>
101103
<scope>import</scope>
102104
</dependency>
103105
<dependency>
104106
<groupId>org.springframework.experimental.boot</groupId>
105107
<artifactId>spring-boot-testjars</artifactId>
106-
<version>0.0.1</version>
108+
<version>${spring-boot-testjars.version}</version>
107109
</dependency>
110+
<dependency>
111+
<groupId>org.instancio</groupId>
112+
<artifactId>instancio-junit</artifactId>
113+
<version>${instancio-junit.version}</version>
114+
<scope>test</scope>
115+
</dependency>
116+
108117
</dependencies>
109118

110119
<build>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.ftm.vcp.bootexamples.domain;
2+
3+
import com.ftm.vcp.bootexamples.testutils.DomainRecordFieldProvider;
4+
import com.ftm.vcp.bootexamples.testutils.SetContextExtension;
5+
import com.ftm.vcp.bootexamples.testutils.SetGenericContext;
6+
import org.instancio.Instancio;
7+
import org.instancio.Select;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.DisplayNameGeneration;
10+
import org.junit.jupiter.api.IndicativeSentencesGeneration;
11+
import org.junit.jupiter.api.TestInstance;
12+
import org.junit.jupiter.api.extension.RegisterExtension;
13+
import org.junit.jupiter.params.ParameterizedTest;
14+
import org.junit.jupiter.params.provider.ArgumentsSource;
15+
16+
import static org.assertj.core.api.Assertions.catchThrowable;
17+
import static org.assertj.core.api.BDDAssertions.then;
18+
import static org.junit.jupiter.api.DisplayNameGenerator.IndicativeSentences;
19+
import static org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
20+
21+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
22+
@DisplayName("A Foo")
23+
@DisplayNameGeneration(IndicativeSentences.class)
24+
@IndicativeSentencesGeneration(separator = " ", generator = ReplaceUnderscores.class)
25+
@SetGenericContext(
26+
value = Foo.class,
27+
errorMessagePattern = "%s cannot be null"
28+
)
29+
class FooTest {
30+
31+
// @RegisterExtension
32+
// private static final SetContextExtension customExtension = new SetContextExtension(
33+
// Foo.class, "%s cannot be null");
34+
35+
@ParameterizedTest(name = "{0}=null, expectedErrorMessage={1}")
36+
@ArgumentsSource(DomainRecordFieldProvider.class)
37+
void should_not_contain_invalid_values(String field, String expectedMessage) {
38+
then(catchThrowable(() -> Instancio.of(Foo.class)
39+
.ignore(Select.field(field))
40+
.create())
41+
)
42+
.hasRootCauseInstanceOf(NullPointerException.class)
43+
.hasRootCauseMessage(expectedMessage);
44+
;
45+
}
46+
47+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.ftm.vcp.bootexamples.testutils;
2+
3+
import org.junit.jupiter.api.extension.ExtensionContext;
4+
import org.junit.jupiter.params.provider.Arguments;
5+
import org.junit.jupiter.params.provider.ArgumentsProvider;
6+
7+
import java.lang.reflect.RecordComponent;
8+
import java.util.Arrays;
9+
import java.util.Optional;
10+
import java.util.stream.Stream;
11+
12+
/**
13+
* Generates arguments as number of fields for a given record, along with an expected error message
14+
*/
15+
public class DomainRecordFieldProvider implements ArgumentsProvider {
16+
17+
@Override
18+
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
19+
final Class<?> contextType = (Class<?>) context.getStore(ExtensionContext.Namespace.GLOBAL).get("contextType");
20+
final String errorMessagePattern = (String) context.getStore(ExtensionContext.Namespace.GLOBAL).get("errorMessagePattern");
21+
return Arrays.stream(contextType.getRecordComponents())
22+
.map(RecordComponent::getName)
23+
.map(fieldName -> new Object() {
24+
final String field = fieldName;
25+
final String expectedMessage = Optional.ofNullable(errorMessagePattern)
26+
.map(string -> string.formatted(field))
27+
.orElseGet(() -> fieldName + " cannot be null");
28+
})
29+
.map(result -> Arguments.arguments(result.field, result.expectedMessage));
30+
}
31+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.ftm.vcp.bootexamples.testutils;
2+
3+
import org.junit.jupiter.api.extension.BeforeAllCallback;
4+
import org.junit.jupiter.api.extension.Extension;
5+
import org.junit.jupiter.api.extension.ExtensionContext;
6+
import org.junit.jupiter.params.provider.ArgumentsProvider;
7+
import org.junit.platform.commons.support.AnnotationSupport;
8+
9+
/**
10+
* Extension allowing to transmit a context (e.g. a generic clas type) to a custom {@link ArgumentsProvider}.
11+
*/
12+
public class SetContextExtension implements BeforeAllCallback, Extension {
13+
14+
private final Class<?> contextType;
15+
private final String errorMessagePattern;
16+
17+
/**
18+
* Used when using
19+
* <code>
20+
*
21+
* @param contextType the context type to set before running the unit test
22+
* @param errorMessagePattern the expected error message pattern
23+
* @RegisterExtension private static final InvalidRecordConstructionExtension customExtension = new InvalidRecordConstructionExtension(Foo.class);
24+
* </code>
25+
*/
26+
public SetContextExtension(Class<?> contextType, String errorMessagePattern) {
27+
this.contextType = contextType;
28+
this.errorMessagePattern = errorMessagePattern;
29+
}
30+
31+
/**
32+
* Used by the {@link SetGenericContext} annotation
33+
*/
34+
public SetContextExtension() {
35+
contextType = null;
36+
errorMessagePattern = null;
37+
}
38+
39+
@Override
40+
public void beforeAll(ExtensionContext context) {
41+
final Class<?> recordType = context.getElement()
42+
.flatMap((annotatedElement) -> AnnotationSupport
43+
.findAnnotation(annotatedElement, SetGenericContext.class))
44+
.<Class<?>>map(SetGenericContext::value)
45+
.orElseGet(() -> contextType);
46+
context.getStore(ExtensionContext.Namespace.GLOBAL)
47+
.put("contextType", recordType);
48+
49+
final String messagePattern = context.getElement()
50+
.flatMap((annotatedElement) -> AnnotationSupport
51+
.findAnnotation(annotatedElement, SetGenericContext.class))
52+
.map(SetGenericContext::errorMessagePattern)
53+
.orElseGet(() -> errorMessagePattern);
54+
context.getStore(ExtensionContext.Namespace.GLOBAL)
55+
.put("errorMessagePattern", messagePattern);
56+
}
57+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ftm.vcp.bootexamples.testutils;
2+
3+
import org.junit.jupiter.api.extension.ExtendWith;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
@Target({ElementType.TYPE})
11+
@Retention(RetentionPolicy.RUNTIME)
12+
@ExtendWith(SetContextExtension.class)
13+
public @interface SetGenericContext {
14+
Class<?> value();
15+
16+
String errorMessagePattern() default "%s is mandatory";
17+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fail.on.error=true

0 commit comments

Comments
 (0)