Skip to content

Commit e0526f3

Browse files
committed
feat: allow strict mode for Enums in TestCases
If a test-method is annotated with `@TestCase`, any parameter of type Enum that is annotated with `@Strict` will be checked whether all enum- values are covered by test-cases. If not, the test will fail at runtime.
1 parent acb4b23 commit e0526f3

File tree

6 files changed

+525
-40
lines changed

6 files changed

+525
-40
lines changed

src/main/java/com/github/nylle/javafixture/annotations/testcases/ReflectedTestCase.java

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ public class ReflectedTestCase {
3131

3232
public ReflectedTestCase(TestCase testCase) {
3333
stream(TestCase.class.getDeclaredMethods())
34-
.sorted(Comparator.comparing(Method::getName))
34+
.sorted(Comparator.comparing(method -> method.getName()))
3535
.forEachOrdered(m -> matrix.compute(m.getReturnType(), (k, v) -> addTo(v, invoke(m, testCase))));
3636
}
3737

3838
@SuppressWarnings("unchecked")
3939
public <T> T getTestCaseValueFor(Class<T> type, int i) {
4040
validate(type, i);
41-
if(type.isEnum()) {
42-
return (T) Enum.valueOf((Class<Enum>) type, (String)matrix.get(asPrimitive(String.class)).get(i));
41+
if (type.isEnum()) {
42+
return (T) Enum.valueOf((Class<Enum>) type, (String) matrix.get(asPrimitive(String.class)).get(i));
4343
}
4444
return (T) matrix.get(asPrimitive(type)).get(i);
4545
}
@@ -83,23 +83,22 @@ private static <T> boolean isInvalid(Class<T> type, List<?> nonDefaultValues) {
8383
if (nonDefaultValues.size() > 1) {
8484
return true;
8585
}
86-
if(type.isEnum()) {
86+
if (type.isEnum()) {
8787
return false;
8888
}
8989
return asPrimitive(type) != asPrimitive(nonDefaultValues.get(0).getClass());
9090
}
9191

9292
private static Class<?> asPrimitive(Class<?> type) {
93-
Map<Class<?>, Class<?>> primitiveWrapperMap = Map.of(
94-
Boolean.class, Boolean.TYPE,
95-
Character.class, Character.TYPE,
96-
Byte.class, Byte.TYPE,
97-
Short.class, Short.TYPE,
98-
Integer.class, Integer.TYPE,
99-
Long.class, Long.TYPE,
100-
Float.class, Float.TYPE,
101-
Double.class, Double.TYPE
102-
);
103-
return primitiveWrapperMap.getOrDefault(type, type);
93+
return Map.<Class<?>, Class<?>>of(
94+
Boolean.class, Boolean.TYPE,
95+
Character.class, Character.TYPE,
96+
Byte.class, Byte.TYPE,
97+
Short.class, Short.TYPE,
98+
Integer.class, Integer.TYPE,
99+
Long.class, Long.TYPE,
100+
Float.class, Float.TYPE,
101+
Double.class, Double.TYPE)
102+
.getOrDefault(type, type);
104103
}
105104
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.github.nylle.javafixture.annotations.testcases;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target(ElementType.PARAMETER)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface Strict {
11+
}
Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package com.github.nylle.javafixture.annotations.testcases;
22

3-
import com.github.nylle.javafixture.Fixture;
43
import com.github.nylle.javafixture.SpecimenType;
54
import org.junit.jupiter.api.extension.ExtensionContext;
65
import org.junit.jupiter.params.provider.Arguments;
76
import org.junit.jupiter.params.provider.ArgumentsProvider;
87
import org.junit.jupiter.params.support.AnnotationConsumer;
98

9+
import java.lang.annotation.Annotation;
1010
import java.lang.reflect.Parameter;
1111
import java.util.List;
12+
import java.util.stream.Collectors;
1213
import java.util.stream.IntStream;
1314
import java.util.stream.Stream;
1415

16+
import static com.github.nylle.javafixture.Fixture.fixture;
1517
import static java.util.Arrays.stream;
1618
import static java.util.stream.Collectors.toList;
19+
import static java.util.stream.Collectors.toMap;
1720

1821
class TestCasesProvider implements ArgumentsProvider, AnnotationConsumer<TestWithCases> {
1922

@@ -23,36 +26,96 @@ public void accept(TestWithCases testWithCases) {
2326

2427
@Override
2528
public Stream<Arguments> provideArguments(ExtensionContext context) {
26-
var fixtures = context.getTestMethod().stream()
27-
.flatMap(method -> stream(method.getParameters())
28-
.filter(parameter -> hasFixtureAnnotation(parameter))
29-
.map(parameter -> parameter.getParameterizedType())
30-
.map(type -> new Fixture().create(SpecimenType.fromClass(type))))
29+
var testMethod = valid(context);
30+
31+
var fixtures = testMethod.parameters()
32+
.filter(parameter -> parameter.getAnnotation(Fixture.class) != null)
33+
.map(parameter -> parameter.getParameterizedType())
34+
.map(type -> fixture().create(SpecimenType.fromClass(type)))
35+
.collect(toList());
36+
37+
var parameterTypes = testMethod.parameters()
38+
.filter(parameter -> parameter.getAnnotation(Fixture.class) == null)
39+
.map(parameter -> parameter.getType())
3140
.collect(toList());
3241

33-
return context.getTestMethod().stream()
34-
.flatMap(method -> stream(method.getDeclaredAnnotations())
35-
.filter(annotation -> annotation.annotationType().equals(TestCases.class))
36-
.map(cases -> (TestCases) cases)
42+
return testMethod.declaredAnnotations(TestCases.class)
3743
.flatMap(testCases -> stream(testCases.value()))
38-
.map(testCase -> mapToArguments(testCase, getParameterTypes(context), fixtures)));
44+
.map(testCase -> new ReflectedTestCase(testCase))
45+
.map(testCase -> IntStream.range(0, parameterTypes.size()).boxed().map(i -> testCase.getTestCaseValueFor(parameterTypes.get(i), i)))
46+
.map(caseValues -> Stream.concat(caseValues, fixtures.stream()))
47+
.map(values -> Arguments.of(values.toArray()));
3948
}
4049

41-
private static Arguments mapToArguments(TestCase testCase, List<Class<?>> parameters, List<Object> fixtures) {
42-
ReflectedTestCase reflectedTestCase = new ReflectedTestCase(testCase);
43-
return Arguments.of(Stream.concat(IntStream.range(0, parameters.size()).boxed()
44-
.map(i -> reflectedTestCase.getTestCaseValueFor(parameters.get(i), i)), fixtures.stream()).toArray());
45-
}
50+
private static TestMethod valid(ExtensionContext context) {
51+
var testMethod = new TestMethod(context);
52+
53+
var parameters = testMethod.parameters().collect(toList());
54+
55+
var strictParamIndices = IntStream.range(0, parameters.size()).boxed().filter(p -> parameters.get(p).getAnnotation(Strict.class) != null).collect(toList());
56+
if (strictParamIndices.isEmpty()) {
57+
return testMethod;
58+
}
59+
60+
var strictAndFixture = strictParamIndices.stream().filter(p -> parameters.get(p).getAnnotation(Fixture.class) != null).collect(toList());
61+
if (!strictAndFixture.isEmpty()) {
62+
throw new TestCaseException("Arguments annotated with @Fixture cannot be @Strict: " +
63+
strictAndFixture.stream()
64+
.map(i -> parameters.get(i).getName())
65+
.collect(Collectors.joining(", ")));
66+
}
67+
68+
var strictAndNotEnum = strictParamIndices.stream().filter(p -> !parameters.get(p).getType().isEnum()).collect(toList());
69+
if (!strictAndNotEnum.isEmpty()) {
70+
throw new TestCaseException("Arguments annotated with @Strict must be of type Enum. The following arguments are not: " +
71+
strictAndNotEnum.stream()
72+
.map(i -> parameters.get(i).getName())
73+
.collect(Collectors.joining(", ")));
74+
}
4675

47-
private static List<Class<?>> getParameterTypes(ExtensionContext context) {
48-
return context.getTestMethod().stream()
49-
.flatMap(method -> stream(method.getParameters())
50-
.filter(parameter -> !hasFixtureAnnotation(parameter))
51-
.map(parameter -> parameter.getType()))
76+
var testCases = testMethod.declaredAnnotations(TestCases.class)
77+
.flatMap(cs -> stream(cs.value()))
78+
.map(c -> new ReflectedTestCase(c))
5279
.collect(toList());
80+
81+
var parameterTypes = IntStream.range(0, parameters.size()).boxed().collect(toMap(k -> k, v -> parameters.get(v).getType()));
82+
83+
strictParamIndices.forEach(i -> {
84+
var values = testCases.stream().map(x -> x.getTestCaseValueFor(parameterTypes.get(i), i)).collect(toList());
85+
var uncovered = stream(((Class<? extends Enum>) parameterTypes.get(i)).getEnumConstants())
86+
.filter(x -> !values.contains(x))
87+
.map(x -> parameterTypes.get(i).getSimpleName() + "." + x)
88+
.collect(Collectors.joining("\n "));
89+
if (!uncovered.isEmpty()) {
90+
throw new AssertionError("@Strict requires all Enum values to be covered by test-cases. Missing values for " + parameters.get(i).getName() + ":\n " + uncovered);
91+
}
92+
});
93+
94+
return testMethod;
5395
}
5496

55-
private static boolean hasFixtureAnnotation(Parameter parameter) {
56-
return parameter.getAnnotation(com.github.nylle.javafixture.annotations.testcases.Fixture.class) != null;
97+
private static class TestMethod {
98+
99+
private final List<Parameter> parameters;
100+
private final List<Annotation> declaredAnnotations;
101+
102+
public TestMethod(ExtensionContext context) {
103+
this.parameters = context.getTestMethod().stream()
104+
.flatMap(method -> Stream.ofNullable(method.getParameters()).flatMap(params -> stream(params)))
105+
.collect(toList());
106+
this.declaredAnnotations = context.getTestMethod().stream()
107+
.flatMap(method -> Stream.ofNullable(method.getDeclaredAnnotations()).flatMap(annotations -> stream(annotations)))
108+
.collect(toList());
109+
}
110+
111+
public Stream<Parameter> parameters() {
112+
return parameters.stream();
113+
}
114+
115+
public <T extends Annotation> Stream<T> declaredAnnotations(Class<T> type) {
116+
return declaredAnnotations.stream()
117+
.filter(x -> x.annotationType().equals(type))
118+
.map(x -> (T) x);
119+
}
57120
}
58121
}

src/test/java/com/github/nylle/javafixture/annotations/testcases/TestCaseExceptionTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,4 @@ void withMessageAndCause() {
1414
assertThat(sut.getCause()).isInstanceOf(NullPointerException.class);
1515
assertThat(sut.getCause().getMessage()).isEqualTo("null");
1616
}
17-
18-
1917
}

src/test/java/com/github/nylle/javafixture/annotations/testcases/TestCaseTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ void canCreateEnumFromString(TestEnum testEnum, String string) {
4646
assertThat(testEnum).isEqualTo(TestEnum.valueOf(string));
4747
}
4848

49+
@TestWithCases
50+
@TestCase(str1 = "VALUE1", str2 = "VALUE1")
51+
@TestCase(str1 = "VALUE2", str2 = "VALUE2")
52+
@TestCase(str1 = "VALUE3", str2 = "VALUE3")
53+
void succeedsWhenAllValuesOfStrictEnumAreCoveredByCases(@Strict TestEnum testEnum, String string) {
54+
assertThat(testEnum).isEqualTo(TestEnum.valueOf(string));
55+
}
56+
4957
@Nested
5058
@DisplayName("verify that primitive wrapper classes are supported as method arguments")
5159
class PrimitiveAutoboxing {

0 commit comments

Comments
 (0)