Skip to content

Commit e21d68d

Browse files
committed
feat: introduce real interface support
If the experimentalInterfaces-flag is enabled, Fixture attempts to find implementing classes for an interface instead of using a proxy as on-the-fly implementation.
1 parent b02168e commit e21d68d

23 files changed

+390
-43
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
<artifactId>jakarta.persistence-api</artifactId>
5959
<version>3.1.0</version>
6060
</dependency>
61+
<dependency>
62+
<groupId>io.github.classgraph</groupId>
63+
<artifactId>classgraph</artifactId>
64+
<version>4.8.164</version>
65+
</dependency>
6166
<dependency>
6267
<groupId>org.junit.jupiter</groupId>
6368
<artifactId>junit-jupiter-engine</artifactId>

src/main/java/com/github/nylle/javafixture/Configuration.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class Configuration {
1010
private int minCollectionSize = 2;
1111
private int streamSize = 3;
1212
private boolean usePositiveNumbersOnly = false;
13+
private boolean experimentalInterfaces = false;
1314

1415
private Clock clock = Clock.fixed(Instant.now(), ZoneOffset.UTC);
1516

@@ -111,6 +112,10 @@ public boolean usePositiveNumbersOnly() {
111112
return this.usePositiveNumbersOnly;
112113
}
113114

115+
public boolean experimentalInterfaces() {
116+
return this.experimentalInterfaces;
117+
}
118+
114119
/**
115120
* @param streamSize the stream size when creating many objects at once
116121
* @return this {@code Configuration}
@@ -151,6 +156,11 @@ public Configuration usePositiveNumbersOnly(boolean usePositiveNumbersOnly) {
151156
return this;
152157
}
153158

159+
public Configuration experimentalInterfaces(boolean experimentalInterfaces) {
160+
this.experimentalInterfaces = experimentalInterfaces;
161+
return this;
162+
}
163+
154164
/**
155165
* Returns a new fixture with this configuration
156166
*

src/main/java/com/github/nylle/javafixture/SpecimenFactory.java

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
import com.github.nylle.javafixture.specimen.SpecialSpecimen;
1414
import com.github.nylle.javafixture.specimen.TimeSpecimen;
1515

16+
import io.github.classgraph.ClassGraph;
17+
import io.github.classgraph.ScanResult;
18+
19+
import java.lang.reflect.Type;
20+
import java.util.Random;
21+
import java.util.stream.IntStream;
22+
23+
import static java.util.stream.Collectors.toMap;
24+
1625
public class SpecimenFactory {
1726

1827
private final Context context;
@@ -23,16 +32,16 @@ public SpecimenFactory(Context context) {
2332

2433
public <T> ISpecimen<T> build(final SpecimenType<T> type) {
2534

26-
if ( context.isCached(type) ) {
27-
return new PredefinedSpecimen<>( type, context );
35+
if (context.isCached(type)) {
36+
return new PredefinedSpecimen<>(type, context);
2837
}
2938

3039
if (type.isPrimitive() || type.isBoxed() || type.asClass() == String.class) {
3140
return new PrimitiveSpecimen<>(type, context);
3241
}
3342

3443
if (type.isEnum()) {
35-
return new EnumSpecimen<>(type );
44+
return new EnumSpecimen<>(type);
3645
}
3746

3847
if (type.isCollection()) {
@@ -43,7 +52,15 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {
4352
return new MapSpecimen<>(type, context, this);
4453
}
4554

46-
if (type.isParameterized()) {
55+
if (type.isParameterized() && !type.isInterface()) {
56+
return new GenericSpecimen<>(type, context, this);
57+
}
58+
59+
if (type.isParameterized() && type.isInterface()) {
60+
if (context.getConfiguration().experimentalInterfaces()) {
61+
return implementationOrProxy(type);
62+
}
63+
4764
return new GenericSpecimen<>(type, context, this);
4865
}
4966

@@ -56,18 +73,57 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {
5673
}
5774

5875
if (type.isInterface()) {
76+
if (context.getConfiguration().experimentalInterfaces()) {
77+
return implementationOrProxy(type);
78+
}
79+
5980
return new InterfaceSpecimen<>(type, context, this);
6081
}
6182

6283
if (type.isAbstract()) {
6384
return new AbstractSpecimen<>(type, context, this);
6485
}
6586

66-
if( type.isSpecialType()) {
87+
if (type.isSpecialType()) {
6788
return new SpecialSpecimen<>(type, context);
6889
}
6990

7091
return new ObjectSpecimen<>(type, context, this);
7192
}
93+
94+
private <T> ISpecimen<T> implementationOrProxy(final SpecimenType<T> interfaceType) {
95+
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
96+
var implementingClasses = scanResult.getClassesImplementing(interfaceType.asClass());
97+
if (implementingClasses.isEmpty()) {
98+
return new InterfaceSpecimen<>(interfaceType, context, this);
99+
}
100+
101+
var implementingClass = implementingClasses.get(new Random().nextInt(implementingClasses.size()));
102+
if (implementingClass.getTypeSignature() == null || implementingClass.getTypeSignature().getTypeParameters().isEmpty()) {
103+
return new ObjectSpecimen<>(SpecimenType.fromClass(implementingClass.loadClass()), context, this);
104+
}
105+
106+
if (!interfaceType.isParameterized()) {
107+
return new InterfaceSpecimen<>(interfaceType, context, this);
108+
}
109+
110+
var typeParameters = IntStream.range(0, interfaceType.getGenericTypeArguments().length)
111+
.boxed()
112+
.collect(toMap(
113+
i -> interfaceType.getTypeParameterName(i),
114+
i -> SpecimenType.fromClass(interfaceType.getGenericTypeArgument(i))));
115+
116+
var actualTypeArguments = implementingClass.getTypeSignature().getTypeParameters().stream()
117+
.map(x -> typeParameters.get(x.getName()).asClass())
118+
.toArray(size -> new Type[size]);
119+
120+
return new GenericSpecimen<>(
121+
SpecimenType.fromRawType(implementingClass.loadClass(), actualTypeArguments),
122+
context,
123+
this);
124+
} catch (Exception ex) {
125+
return new InterfaceSpecimen<>(interfaceType, context, this);
126+
}
127+
}
72128
}
73129

src/test/java/com/github/nylle/javafixture/ConfigurationTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,4 @@ void canCreateAFixtureWithGivenConfiguration() {
1818

1919
assertThat(result).hasSize(1);
2020
}
21-
2221
}

src/test/java/com/github/nylle/javafixture/FixtureTest.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.github.nylle.javafixture.testobjects.ITestGeneric;
44
import com.github.nylle.javafixture.testobjects.ITestGenericInside;
55
import com.github.nylle.javafixture.testobjects.TestEnum;
6-
import com.github.nylle.javafixture.testobjects.TestInterface;
76
import com.github.nylle.javafixture.testobjects.TestObjectGeneric;
87
import com.github.nylle.javafixture.testobjects.TestObjectWithDeepNesting;
98
import com.github.nylle.javafixture.testobjects.TestObjectWithEnumMap;
@@ -17,6 +16,9 @@
1716
import com.github.nylle.javafixture.testobjects.example.Contract;
1817
import com.github.nylle.javafixture.testobjects.example.ContractCategory;
1918
import com.github.nylle.javafixture.testobjects.example.ContractPosition;
19+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationU;
20+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationUImpl;
21+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithoutImplementation;
2022
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithGenericConstructor;
2123
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithoutDefaultConstructor;
2224
import org.assertj.core.api.SoftAssertions;
@@ -261,7 +263,7 @@ void objectCanBeCustomizedWithType() {
261263
void interfaceCanBeCustomizedWithType() {
262264
Fixture sut = new Fixture(configuration);
263265

264-
var result = sut.build(TestInterface.class)
266+
var result = sut.build(InterfaceWithoutImplementation.class)
265267
.with(String.class, "expected")
266268
.create();
267269

@@ -413,6 +415,7 @@ void createThroughRandomConstructor() {
413415
@Nested
414416
@DisplayName("when using SpecimenType<T>")
415417
class WhenSpecimenType {
418+
416419
@Test
417420
void canCreateGenericObject() {
418421
Fixture fixture = new Fixture(configuration);
@@ -441,6 +444,18 @@ void canCreateGenericInterface() {
441444
assertThat(result.getU().getTestGeneric().getU()).isInstanceOf(Integer.class);
442445
}
443446

447+
@Test
448+
void canCreateGenericObjectFromInterfaceWithMismatchingNumberOfTypeParameters() {
449+
var fixture = new Fixture(Configuration.configure().experimentalInterfaces(true));
450+
451+
var result = fixture.create(new SpecimenType<GenericInterfaceTUWithGenericImplementationU<String, Integer>>() {});
452+
453+
assertThat(result).isInstanceOf(GenericInterfaceTUWithGenericImplementationUImpl.class);
454+
assertThat(result.publicField).isInstanceOf(Integer.class);
455+
assertThat(result.getT()).isInstanceOf(String.class);
456+
assertThat(result.getU()).isInstanceOf(Integer.class);
457+
}
458+
444459
@Test
445460
void canCreateMapsAndLists() {
446461
Fixture fixture = new Fixture(configuration);
@@ -554,7 +569,6 @@ void createThroughRandomConstructor() {
554569
assertThat(result.getValue()).isInstanceOf(String.class);
555570
assertThat(result.getInteger()).isInstanceOf(Optional.class);
556571
}
557-
558572
}
559573

560574
@Test

src/test/java/com/github/nylle/javafixture/SpecimenFactoryTest.java

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@
1717
import com.github.nylle.javafixture.testobjects.TestObjectGeneric;
1818
import com.github.nylle.javafixture.testobjects.TestPrimitive;
1919
import com.github.nylle.javafixture.testobjects.example.IContract;
20+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationT;
21+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationTU;
22+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationU;
23+
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceWithImplementation;
24+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithGenericImplementation;
25+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithImplementation;
26+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithoutImplementation;
2027
import org.junit.jupiter.api.DisplayName;
28+
import org.junit.jupiter.api.Nested;
2129
import org.junit.jupiter.api.Test;
2230

2331
import java.io.File;
@@ -65,23 +73,100 @@ void build(Class<?> value, Class<?> expected) {
6573
void buildGeneric() {
6674
var sut = new SpecimenFactory(new Context(new Configuration()));
6775

68-
assertThat(sut.build(new SpecimenType<List<String>>(){})).isExactlyInstanceOf(CollectionSpecimen.class);
69-
assertThat(sut.build(new SpecimenType<Map<String, Integer>>(){})).isExactlyInstanceOf(MapSpecimen.class);
70-
assertThat(sut.build(new SpecimenType<Class<String>>(){})).isExactlyInstanceOf(GenericSpecimen.class);
71-
assertThat(sut.build(new SpecimenType<TestObjectGeneric<String, List<Integer>>>(){})).isExactlyInstanceOf(GenericSpecimen.class);
76+
assertThat(sut.build(new SpecimenType<List<String>>() {})).isExactlyInstanceOf(CollectionSpecimen.class);
77+
assertThat(sut.build(new SpecimenType<Map<String, Integer>>() {})).isExactlyInstanceOf(MapSpecimen.class);
78+
assertThat(sut.build(new SpecimenType<Class<String>>() {})).isExactlyInstanceOf(GenericSpecimen.class);
79+
assertThat(sut.build(new SpecimenType<TestObjectGeneric<String, List<Integer>>>() {})).isExactlyInstanceOf(GenericSpecimen.class);
7280
}
7381

7482
@Test
75-
@DisplayName( "when cache contains a predefined value, return this" )
76-
void buildReturnsCacnedValue() {
77-
var context = new Context( new Configuration() );
83+
@DisplayName("when cache contains a predefined value, return this")
84+
void buildReturnsCachedValue() {
85+
var context = new Context(new Configuration());
7886
var cachedValue = new TestPrimitive();
79-
var type = SpecimenType.fromClass( TestPrimitive.class );
80-
context.overwrite( type, cachedValue );
81-
var sut = new SpecimenFactory( context );
87+
var type = SpecimenType.fromClass(TestPrimitive.class);
88+
context.overwrite(type, cachedValue);
89+
var sut = new SpecimenFactory(context);
8290

83-
assertThat( sut.build( type ) ).isExactlyInstanceOf( PredefinedSpecimen.class );
91+
assertThat(sut.build(type)).isExactlyInstanceOf(PredefinedSpecimen.class);
8492
}
8593

94+
@Nested
95+
class Interfaces {
8696

97+
Context context = new Context(Configuration.configure().experimentalInterfaces(true));
98+
99+
@TestWithCases
100+
@TestCase(bool1 = true, class2 = ObjectSpecimen.class)
101+
@TestCase(bool1 = false, class2 = InterfaceSpecimen.class)
102+
void interfaceImplementationsAreOnlySupportedIfExperimentalInterfacesAreEnabled(boolean experimental, Class<?> expected) {
103+
var context = new Context(Configuration.configure().experimentalInterfaces(experimental));
104+
105+
assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithImplementation.class))).isExactlyInstanceOf(expected);
106+
}
107+
108+
@Nested
109+
@DisplayName("creates InterfaceSpecimen if")
110+
class CreatesInterfaceSpecimen {
111+
112+
@Test
113+
@DisplayName("no implementations found")
114+
void createsInterfaceSpecimenIfInterfaceHasNoImplementations() {
115+
assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithoutImplementation.class)))
116+
.isExactlyInstanceOf(InterfaceSpecimen.class);
117+
}
118+
119+
@Test
120+
@DisplayName("implementation is generic and interface is not")
121+
void createsInterfaceSpecimenIfImplementationIsGenericAndInterfaceIsNot() {
122+
assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithGenericImplementation.class)))
123+
.isExactlyInstanceOf(InterfaceSpecimen.class);
124+
}
125+
}
126+
127+
@Nested
128+
@DisplayName("creates ObjectSpecimen if")
129+
class CreatesObjectSpecimen {
130+
131+
@Test
132+
@DisplayName("implementation is not generic and interface is not generic")
133+
void createsObjectSpecimenIfBothImplementationAndInterfaceAreNotGeneric() {
134+
assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithImplementation.class)))
135+
.isExactlyInstanceOf(ObjectSpecimen.class);
136+
}
137+
138+
@Test
139+
@DisplayName("implementation is not generic and interface is generic")
140+
void createsObjectSpecimenIfImplementationIsNotGenericAndInterfaceIs() {
141+
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceWithImplementation<Integer, String>>() {}))
142+
.isExactlyInstanceOf(ObjectSpecimen.class);
143+
}
144+
}
145+
146+
@Nested
147+
@DisplayName("creates GenericSpecimen if")
148+
class CreatesGenericSpecimen {
149+
150+
@Test
151+
@DisplayName("implementation is generic and interface is generic")
152+
void createsGenericSpecimenIfImplementationAndInterfaceAreGeneric() {
153+
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceTUWithGenericImplementationTU<String, Integer>>() {}))
154+
.isExactlyInstanceOf(GenericSpecimen.class);
155+
}
156+
157+
@Test
158+
@DisplayName("generic implementation only uses first type-argument of generic interface")
159+
void createsGenericSpecimenIfGenericImplementationOnlyUsesFirstTypeArgumentOfGenericInterface() {
160+
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceTUWithGenericImplementationT<String, Integer>>() {}))
161+
.isExactlyInstanceOf(GenericSpecimen.class);
162+
}
163+
164+
@Test
165+
@DisplayName("generic implementation only uses second type-argument of generic interface")
166+
void createsGenericSpecimenIfGenericImplementationOnlyUsesSecondTypeArgumentOfGenericInterface() {
167+
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceTUWithGenericImplementationU<String, Integer>>() {}))
168+
.isExactlyInstanceOf(GenericSpecimen.class);
169+
}
170+
}
171+
}
87172
}

src/test/java/com/github/nylle/javafixture/specimen/AbstractSpecimenTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.github.nylle.javafixture.SpecimenFactory;
66
import com.github.nylle.javafixture.SpecimenType;
77
import com.github.nylle.javafixture.testobjects.TestAbstractClass;
8-
import com.github.nylle.javafixture.testobjects.TestInterface;
8+
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithoutImplementation;
99
import org.junit.jupiter.api.BeforeEach;
1010
import org.junit.jupiter.api.Test;
1111

@@ -44,14 +44,14 @@ void typeIsRequired() {
4444

4545
@Test
4646
void contextIsRequired() {
47-
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(TestInterface.class), null, specimenFactory))
47+
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(InterfaceWithoutImplementation.class), null, specimenFactory))
4848
.isInstanceOf(IllegalArgumentException.class)
4949
.hasMessageContaining("context: null");
5050
}
5151

5252
@Test
5353
void specimenFactoryIsRequired() {
54-
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(TestInterface.class), context, null))
54+
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(InterfaceWithoutImplementation.class), context, null))
5555
.isInstanceOf(IllegalArgumentException.class)
5656
.hasMessageContaining("specimenFactory: null");
5757
}

0 commit comments

Comments
 (0)