Skip to content

Commit

Permalink
feat: introduce constructor instantiator
Browse files Browse the repository at this point in the history
  • Loading branch information
Nylle committed Sep 8, 2024
1 parent f0ae1ac commit b9421db
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.github.nylle.javafixture.instantiation;

import com.github.nylle.javafixture.CustomizationContext;
import com.github.nylle.javafixture.SpecimenFactory;
import com.github.nylle.javafixture.SpecimenType;

import java.lang.annotation.Annotation;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.Map;

import static java.util.Arrays.stream;

public class Constructor<T> implements Instantiator<T> {

private final java.lang.reflect.Constructor<T> constructor;

private Constructor(java.lang.reflect.Constructor<T> constructor) {
this.constructor = constructor;
}

public static <T> Constructor<T> create(java.lang.reflect.Constructor<T> constructor) {
return new Constructor<>(constructor);
}

public T invoke(SpecimenFactory specimenFactory, CustomizationContext customizationContext) {
try {
return constructor.newInstance(stream(constructor.getParameters())
.map(p -> createParameter(p, specimenFactory, customizationContext))
.toArray());
} catch(Exception ex) {
return null;
}
}

private static Object createParameter(Parameter parameter, SpecimenFactory specimenFactory, CustomizationContext customizationContext) {
if (customizationContext.getIgnoredFields().contains(parameter.getName())) {
return Primitive.defaultValue(parameter.getType());
}
if (customizationContext.getCustomFields().containsKey(parameter.getName())) {
return customizationContext.getCustomFields().get(parameter.getName());
}
return specimenFactory
.build(SpecimenType.fromClass(parameter.getParameterizedType()))
.create(new CustomizationContext(List.of(), Map.of(), customizationContext.useRandomConstructor()), new Annotation[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.github.nylle.javafixture.instantiation;

import java.util.Map;

public class Primitive {
private static final Map<Class<?>, Object> primitiveDefaults = Map.of(
Boolean.TYPE, false,
Character.TYPE, '\0',
Byte.TYPE, (byte) 0,
Short.TYPE, 0,
Integer.TYPE, 0,
Long.TYPE, 0L,
Float.TYPE, 0.0f,
Double.TYPE, 0.0d
);

private Primitive() {
}

public static Object defaultValue(Class<?> type) {
return primitiveDefaults.get(type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.github.nylle.javafixture.instantiation;

import com.github.nylle.javafixture.Configuration;
import com.github.nylle.javafixture.Context;
import com.github.nylle.javafixture.CustomizationContext;
import com.github.nylle.javafixture.SpecimenFactory;
import com.github.nylle.javafixture.testobjects.TestObject;
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithConstructedField;
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithGenericConstructor;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

class ConstructorTest {

@Test
@DisplayName("returns instance")
void canCreateInstanceFromConstructor() throws NoSuchMethodException {
var sut = Constructor.create(TestObjectWithGenericConstructor.class.getConstructor(String.class, Optional.class));

var actual = sut.invoke(new SpecimenFactory(new Context(Configuration.configure())), new CustomizationContext(List.of(), Map.of(), false));

assertThat(actual).isInstanceOf(TestObjectWithGenericConstructor.class);
assertThat(actual.getValue()).isInstanceOf(String.class);
assertThat(actual.getInteger()).isInstanceOf(Optional.class);
}

@Test
@DisplayName("fields not set by constructor are null")
void fieldsNotSetByConstructorAreNull() throws NoSuchMethodException {
var sut = Constructor.create(TestObjectWithGenericConstructor.class.getConstructor(String.class, Optional.class));

var actual = sut.invoke(new SpecimenFactory(new Context(Configuration.configure())), new CustomizationContext(List.of(), Map.of(), false));

assertThat(actual).isInstanceOf(TestObjectWithGenericConstructor.class);
assertThat(actual.getPrivateField()).isNull();
}

@Test
@DisplayName("arguments can be customized")
void argumentsCanBeCustomized() throws NoSuchMethodException {
var sut = Constructor.create(TestObject.class.getConstructor(String.class, List.class, Map.class));

// use arg0, because .class files do not store formal parameter names by default
var actual = sut.invoke(new SpecimenFactory(new Context(Configuration.configure())), new CustomizationContext(List.of(), Map.of("arg0", "customized"), true));

assertThat(actual.getValue()).isEqualTo("customized");
}

@Test
@DisplayName("using constructor is used for all instances")
void usingConstructorIsRecursive() throws NoSuchMethodException {
var sut = Constructor.create(TestObjectWithConstructedField.class.getConstructor(int.class, TestObjectWithGenericConstructor.class));

var actual = sut.invoke(new SpecimenFactory(new Context(Configuration.configure())), new CustomizationContext(List.of(), Map.of(), true));

assertThat(actual).isInstanceOf(TestObjectWithConstructedField.class);
assertThat(actual.getTestObjectWithGenericConstructor().getPrivateField()).isNull();
assertThat(actual.getNotSetByConstructor()).isNull();
}

@Test
@DisplayName("customized arguments are only used for the top level object (no nested objects)")
void constructorArgumentsAreUsedOnce() throws NoSuchMethodException {
var sut = Constructor.create(TestObjectWithConstructedField.class.getConstructor(int.class, TestObjectWithGenericConstructor.class));

// use arg0, because .class files do not store formal parameter names by default
var actual = sut.invoke(new SpecimenFactory(new Context(Configuration.configure())), new CustomizationContext(List.of(), Map.of("arg0", 2), true));

assertThat(actual.getSetByConstructor()).isEqualTo(2);
}

@Test
@DisplayName("customized arguments are used for exclusion, too")
void ignoredConstructorArgsAreRespected() throws NoSuchMethodException {
var sut = Constructor.create(TestObjectWithConstructedField.class.getConstructor(int.class, TestObjectWithGenericConstructor.class));

// use arg0, because .class files do not store formal parameter names by default
var actual = sut.invoke(new SpecimenFactory(new Context(Configuration.configure())), new CustomizationContext(List.of("arg0"), Map.of(), true));

assertThat(actual.getSetByConstructor()).isEqualTo(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.github.nylle.javafixture.instantiation;

import com.github.nylle.javafixture.annotations.testcases.TestCase;
import com.github.nylle.javafixture.annotations.testcases.TestWithCases;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class PrimitiveTest {

@Test
void returnsDefaultBoolean() {
assertThat(Primitive.defaultValue(boolean.class)).isEqualTo(false);
}

@Test
void returnsDefaultCharacter() {
assertThat(Primitive.defaultValue(char.class)).isEqualTo('\0');
}

@Test
void returnsDefaultByte() {
assertThat(Primitive.defaultValue(byte.class)).isEqualTo((byte) 0);
}

@TestWithCases
@TestCase(class1 = short.class)
@TestCase(class1 = int.class)
void returnsDefaultInteger(Class<?> type) {
assertThat(Primitive.defaultValue(type)).isEqualTo(0);
}

@Test
void returnsDefaultLong() {
assertThat(Primitive.defaultValue(long.class)).isEqualTo(0L);
}

@Test
void returnsDefaultFloat() {
assertThat(Primitive.defaultValue(float.class)).isEqualTo(0.0f);
}

@Test
void returnsDefaultDouble() {
assertThat(Primitive.defaultValue(double.class)).isEqualTo(0.0d);
}

@TestWithCases
@TestCase(class1 = Boolean.class, bool2 = true)
@TestCase(class1 = Character.class, bool2 = true)
@TestCase(class1 = Byte.class, bool2 = true)
@TestCase(class1 = Short.class, bool2 = true)
@TestCase(class1 = Integer.class, bool2 = true)
@TestCase(class1 = Long.class, bool2 = true)
@TestCase(class1 = Float.class, bool2 = true)
@TestCase(class1 = Double.class, bool2 = true)
@TestCase(class1 = boolean.class, bool2 = false)
@TestCase(class1 = char.class, bool2 = false)
@TestCase(class1 = byte.class, bool2 = false)
@TestCase(class1 = short.class, bool2 = false)
@TestCase(class1 = int.class, bool2 = false)
@TestCase(class1 = long.class, bool2 = false)
@TestCase(class1 = float.class, bool2 = false)
@TestCase(class1 = double.class, bool2 = false)
void returnsNullForBoxedPrimitive(Class<?> type, boolean isNull) {
assertThat(Primitive.defaultValue(type) == null).isEqualTo(isNull);
}
}

0 comments on commit b9421db

Please sign in to comment.