Skip to content

Commit

Permalink
feat: introduce real interface support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Nylle committed Nov 17, 2023
1 parent b02168e commit e21d68d
Show file tree
Hide file tree
Showing 23 changed files with 390 additions and 43 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.164</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/github/nylle/javafixture/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class Configuration {
private int minCollectionSize = 2;
private int streamSize = 3;
private boolean usePositiveNumbersOnly = false;
private boolean experimentalInterfaces = false;

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

Expand Down Expand Up @@ -111,6 +112,10 @@ public boolean usePositiveNumbersOnly() {
return this.usePositiveNumbersOnly;
}

public boolean experimentalInterfaces() {
return this.experimentalInterfaces;
}

/**
* @param streamSize the stream size when creating many objects at once
* @return this {@code Configuration}
Expand Down Expand Up @@ -151,6 +156,11 @@ public Configuration usePositiveNumbersOnly(boolean usePositiveNumbersOnly) {
return this;
}

public Configuration experimentalInterfaces(boolean experimentalInterfaces) {
this.experimentalInterfaces = experimentalInterfaces;
return this;
}

/**
* Returns a new fixture with this configuration
*
Expand Down
66 changes: 61 additions & 5 deletions src/main/java/com/github/nylle/javafixture/SpecimenFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
import com.github.nylle.javafixture.specimen.SpecialSpecimen;
import com.github.nylle.javafixture.specimen.TimeSpecimen;

import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;

import java.lang.reflect.Type;
import java.util.Random;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.toMap;

public class SpecimenFactory {

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

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

if ( context.isCached(type) ) {
return new PredefinedSpecimen<>( type, context );
if (context.isCached(type)) {
return new PredefinedSpecimen<>(type, context);
}

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

if (type.isEnum()) {
return new EnumSpecimen<>(type );
return new EnumSpecimen<>(type);
}

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

if (type.isParameterized()) {
if (type.isParameterized() && !type.isInterface()) {
return new GenericSpecimen<>(type, context, this);
}

if (type.isParameterized() && type.isInterface()) {
if (context.getConfiguration().experimentalInterfaces()) {
return implementationOrProxy(type);
}

return new GenericSpecimen<>(type, context, this);
}

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

if (type.isInterface()) {
if (context.getConfiguration().experimentalInterfaces()) {
return implementationOrProxy(type);
}

return new InterfaceSpecimen<>(type, context, this);
}

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

if( type.isSpecialType()) {
if (type.isSpecialType()) {
return new SpecialSpecimen<>(type, context);
}

return new ObjectSpecimen<>(type, context, this);
}

private <T> ISpecimen<T> implementationOrProxy(final SpecimenType<T> interfaceType) {
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
var implementingClasses = scanResult.getClassesImplementing(interfaceType.asClass());
if (implementingClasses.isEmpty()) {
return new InterfaceSpecimen<>(interfaceType, context, this);
}

var implementingClass = implementingClasses.get(new Random().nextInt(implementingClasses.size()));
if (implementingClass.getTypeSignature() == null || implementingClass.getTypeSignature().getTypeParameters().isEmpty()) {
return new ObjectSpecimen<>(SpecimenType.fromClass(implementingClass.loadClass()), context, this);
}

if (!interfaceType.isParameterized()) {
return new InterfaceSpecimen<>(interfaceType, context, this);
}

var typeParameters = IntStream.range(0, interfaceType.getGenericTypeArguments().length)
.boxed()
.collect(toMap(
i -> interfaceType.getTypeParameterName(i),
i -> SpecimenType.fromClass(interfaceType.getGenericTypeArgument(i))));

var actualTypeArguments = implementingClass.getTypeSignature().getTypeParameters().stream()
.map(x -> typeParameters.get(x.getName()).asClass())
.toArray(size -> new Type[size]);

return new GenericSpecimen<>(
SpecimenType.fromRawType(implementingClass.loadClass(), actualTypeArguments),
context,
this);
} catch (Exception ex) {
return new InterfaceSpecimen<>(interfaceType, context, this);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ void canCreateAFixtureWithGivenConfiguration() {

assertThat(result).hasSize(1);
}

}
20 changes: 17 additions & 3 deletions src/test/java/com/github/nylle/javafixture/FixtureTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.github.nylle.javafixture.testobjects.ITestGeneric;
import com.github.nylle.javafixture.testobjects.ITestGenericInside;
import com.github.nylle.javafixture.testobjects.TestEnum;
import com.github.nylle.javafixture.testobjects.TestInterface;
import com.github.nylle.javafixture.testobjects.TestObjectGeneric;
import com.github.nylle.javafixture.testobjects.TestObjectWithDeepNesting;
import com.github.nylle.javafixture.testobjects.TestObjectWithEnumMap;
Expand All @@ -17,6 +16,9 @@
import com.github.nylle.javafixture.testobjects.example.Contract;
import com.github.nylle.javafixture.testobjects.example.ContractCategory;
import com.github.nylle.javafixture.testobjects.example.ContractPosition;
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationU;
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationUImpl;
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithoutImplementation;
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithGenericConstructor;
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithoutDefaultConstructor;
import org.assertj.core.api.SoftAssertions;
Expand Down Expand Up @@ -261,7 +263,7 @@ void objectCanBeCustomizedWithType() {
void interfaceCanBeCustomizedWithType() {
Fixture sut = new Fixture(configuration);

var result = sut.build(TestInterface.class)
var result = sut.build(InterfaceWithoutImplementation.class)
.with(String.class, "expected")
.create();

Expand Down Expand Up @@ -413,6 +415,7 @@ void createThroughRandomConstructor() {
@Nested
@DisplayName("when using SpecimenType<T>")
class WhenSpecimenType {

@Test
void canCreateGenericObject() {
Fixture fixture = new Fixture(configuration);
Expand Down Expand Up @@ -441,6 +444,18 @@ void canCreateGenericInterface() {
assertThat(result.getU().getTestGeneric().getU()).isInstanceOf(Integer.class);
}

@Test
void canCreateGenericObjectFromInterfaceWithMismatchingNumberOfTypeParameters() {
var fixture = new Fixture(Configuration.configure().experimentalInterfaces(true));

var result = fixture.create(new SpecimenType<GenericInterfaceTUWithGenericImplementationU<String, Integer>>() {});

assertThat(result).isInstanceOf(GenericInterfaceTUWithGenericImplementationUImpl.class);
assertThat(result.publicField).isInstanceOf(Integer.class);
assertThat(result.getT()).isInstanceOf(String.class);
assertThat(result.getU()).isInstanceOf(Integer.class);
}

@Test
void canCreateMapsAndLists() {
Fixture fixture = new Fixture(configuration);
Expand Down Expand Up @@ -554,7 +569,6 @@ void createThroughRandomConstructor() {
assertThat(result.getValue()).isInstanceOf(String.class);
assertThat(result.getInteger()).isInstanceOf(Optional.class);
}

}

@Test
Expand Down
107 changes: 96 additions & 11 deletions src/test/java/com/github/nylle/javafixture/SpecimenFactoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@
import com.github.nylle.javafixture.testobjects.TestObjectGeneric;
import com.github.nylle.javafixture.testobjects.TestPrimitive;
import com.github.nylle.javafixture.testobjects.example.IContract;
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationT;
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationTU;
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceTUWithGenericImplementationU;
import com.github.nylle.javafixture.testobjects.interfaces.GenericInterfaceWithImplementation;
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithGenericImplementation;
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithImplementation;
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithoutImplementation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

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

assertThat(sut.build(new SpecimenType<List<String>>(){})).isExactlyInstanceOf(CollectionSpecimen.class);
assertThat(sut.build(new SpecimenType<Map<String, Integer>>(){})).isExactlyInstanceOf(MapSpecimen.class);
assertThat(sut.build(new SpecimenType<Class<String>>(){})).isExactlyInstanceOf(GenericSpecimen.class);
assertThat(sut.build(new SpecimenType<TestObjectGeneric<String, List<Integer>>>(){})).isExactlyInstanceOf(GenericSpecimen.class);
assertThat(sut.build(new SpecimenType<List<String>>() {})).isExactlyInstanceOf(CollectionSpecimen.class);
assertThat(sut.build(new SpecimenType<Map<String, Integer>>() {})).isExactlyInstanceOf(MapSpecimen.class);
assertThat(sut.build(new SpecimenType<Class<String>>() {})).isExactlyInstanceOf(GenericSpecimen.class);
assertThat(sut.build(new SpecimenType<TestObjectGeneric<String, List<Integer>>>() {})).isExactlyInstanceOf(GenericSpecimen.class);
}

@Test
@DisplayName( "when cache contains a predefined value, return this" )
void buildReturnsCacnedValue() {
var context = new Context( new Configuration() );
@DisplayName("when cache contains a predefined value, return this")
void buildReturnsCachedValue() {
var context = new Context(new Configuration());
var cachedValue = new TestPrimitive();
var type = SpecimenType.fromClass( TestPrimitive.class );
context.overwrite( type, cachedValue );
var sut = new SpecimenFactory( context );
var type = SpecimenType.fromClass(TestPrimitive.class);
context.overwrite(type, cachedValue);
var sut = new SpecimenFactory(context);

assertThat( sut.build( type ) ).isExactlyInstanceOf( PredefinedSpecimen.class );
assertThat(sut.build(type)).isExactlyInstanceOf(PredefinedSpecimen.class);
}

@Nested
class Interfaces {

Context context = new Context(Configuration.configure().experimentalInterfaces(true));

@TestWithCases
@TestCase(bool1 = true, class2 = ObjectSpecimen.class)
@TestCase(bool1 = false, class2 = InterfaceSpecimen.class)
void interfaceImplementationsAreOnlySupportedIfExperimentalInterfacesAreEnabled(boolean experimental, Class<?> expected) {
var context = new Context(Configuration.configure().experimentalInterfaces(experimental));

assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithImplementation.class))).isExactlyInstanceOf(expected);
}

@Nested
@DisplayName("creates InterfaceSpecimen if")
class CreatesInterfaceSpecimen {

@Test
@DisplayName("no implementations found")
void createsInterfaceSpecimenIfInterfaceHasNoImplementations() {
assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithoutImplementation.class)))
.isExactlyInstanceOf(InterfaceSpecimen.class);
}

@Test
@DisplayName("implementation is generic and interface is not")
void createsInterfaceSpecimenIfImplementationIsGenericAndInterfaceIsNot() {
assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithGenericImplementation.class)))
.isExactlyInstanceOf(InterfaceSpecimen.class);
}
}

@Nested
@DisplayName("creates ObjectSpecimen if")
class CreatesObjectSpecimen {

@Test
@DisplayName("implementation is not generic and interface is not generic")
void createsObjectSpecimenIfBothImplementationAndInterfaceAreNotGeneric() {
assertThat(new SpecimenFactory(context).build(SpecimenType.fromClass(InterfaceWithImplementation.class)))
.isExactlyInstanceOf(ObjectSpecimen.class);
}

@Test
@DisplayName("implementation is not generic and interface is generic")
void createsObjectSpecimenIfImplementationIsNotGenericAndInterfaceIs() {
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceWithImplementation<Integer, String>>() {}))
.isExactlyInstanceOf(ObjectSpecimen.class);
}
}

@Nested
@DisplayName("creates GenericSpecimen if")
class CreatesGenericSpecimen {

@Test
@DisplayName("implementation is generic and interface is generic")
void createsGenericSpecimenIfImplementationAndInterfaceAreGeneric() {
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceTUWithGenericImplementationTU<String, Integer>>() {}))
.isExactlyInstanceOf(GenericSpecimen.class);
}

@Test
@DisplayName("generic implementation only uses first type-argument of generic interface")
void createsGenericSpecimenIfGenericImplementationOnlyUsesFirstTypeArgumentOfGenericInterface() {
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceTUWithGenericImplementationT<String, Integer>>() {}))
.isExactlyInstanceOf(GenericSpecimen.class);
}

@Test
@DisplayName("generic implementation only uses second type-argument of generic interface")
void createsGenericSpecimenIfGenericImplementationOnlyUsesSecondTypeArgumentOfGenericInterface() {
assertThat(new SpecimenFactory(context).build(new SpecimenType<GenericInterfaceTUWithGenericImplementationU<String, Integer>>() {}))
.isExactlyInstanceOf(GenericSpecimen.class);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.github.nylle.javafixture.SpecimenFactory;
import com.github.nylle.javafixture.SpecimenType;
import com.github.nylle.javafixture.testobjects.TestAbstractClass;
import com.github.nylle.javafixture.testobjects.TestInterface;
import com.github.nylle.javafixture.testobjects.interfaces.InterfaceWithoutImplementation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -44,14 +44,14 @@ void typeIsRequired() {

@Test
void contextIsRequired() {
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(TestInterface.class), null, specimenFactory))
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(InterfaceWithoutImplementation.class), null, specimenFactory))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("context: null");
}

@Test
void specimenFactoryIsRequired() {
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(TestInterface.class), context, null))
assertThatThrownBy(() -> new AbstractSpecimen<>(SpecimenType.fromClass(InterfaceWithoutImplementation.class), context, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("specimenFactory: null");
}
Expand Down
Loading

0 comments on commit e21d68d

Please sign in to comment.