Skip to content

Commit 595b13c

Browse files
committed
fix: pick constructor matching customization
When a field is customized, using a random constructor might pick one that does not contain the customized field therefore ignoring the customization. Instead, we select all constructors that contain the customized fields and pick a random one out of those. If no constructor with the customized fields is found, an exception is thrown. This will only work for records since traditional classes do not store proper constructor argument names (instead use arg0, arg1, etc.) and cannot match those to customized field-names.
1 parent e6b47ef commit 595b13c

File tree

3 files changed

+117
-3
lines changed

3 files changed

+117
-3
lines changed

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.lang.reflect.Parameter;
1515
import java.util.ArrayDeque;
1616
import java.util.ArrayList;
17+
import java.util.Arrays;
1718
import java.util.Collection;
1819
import java.util.Deque;
1920
import java.util.HashMap;
@@ -32,9 +33,12 @@
3233
import java.util.concurrent.LinkedBlockingQueue;
3334
import java.util.concurrent.LinkedTransferQueue;
3435
import java.util.concurrent.TransferQueue;
36+
import java.util.stream.Stream;
3537

3638
import static java.lang.String.format;
3739
import static java.util.Arrays.stream;
40+
import static java.util.stream.Collectors.joining;
41+
import static java.util.stream.Collectors.toList;
3842

3943
public class InstanceFactory {
4044

@@ -58,9 +62,8 @@ public InstanceFactory(SpecimenFactory specimenFactory) {
5862
}
5963

6064
public <T> T construct(SpecimenType<T> type, CustomizationContext customizationContext) {
61-
return random.shuffled(type.getDeclaredConstructors())
65+
return random.shuffled(findValidConstructors(type, customizationContext))
6266
.stream()
63-
.filter(x -> Modifier.isPublic(x.getModifiers()))
6467
.findFirst()
6568
.map(x -> construct(type, x, customizationContext))
6669
.orElseGet(() -> manufacture(type, customizationContext, new SpecimenException("No public constructor found")));
@@ -223,4 +226,42 @@ private <G, T extends Collection<G>> T createCollectionFromConcreteType(final Sp
223226
throw new SpecimenException("Unable to create collection of type " + type.getName(), e);
224227
}
225228
}
229+
230+
private static <T> List<Constructor<T>> findValidConstructors(SpecimenType<T> type, CustomizationContext customizationContext) {
231+
var publicConstructors = type.getDeclaredConstructors().stream().filter(x -> Modifier.isPublic(x.getModifiers())).collect(toList());
232+
233+
if (publicConstructors.isEmpty()) {
234+
return publicConstructors;
235+
}
236+
237+
if (customizationContext.getCustomFields().isEmpty() && customizationContext.getIgnoredFields().isEmpty()) {
238+
return publicConstructors;
239+
}
240+
241+
var candidates = publicConstructors.stream()
242+
.map(constructor -> {
243+
var parameterNames = stream(constructor.getParameters()).map(x -> x.getName()).collect(toList());
244+
var missingParameters = customizationContext.findAllCustomizedFieldNames()
245+
.map(entry -> entry.replaceAll("\\..+", ""))
246+
.filter(entry -> !parameterNames.contains(entry))
247+
.collect(toList());
248+
return Map.<Constructor<T>, List<String>>entry(constructor, missingParameters);
249+
})
250+
.collect(toList());
251+
252+
253+
var result = candidates.stream().filter(x -> x.getValue().isEmpty()).map(x -> x.getKey()).collect(toList());
254+
255+
if (result.isEmpty()) {
256+
throw new SpecimenException(Stream.concat(
257+
Stream.of("Cannot customize fields because no suitable constructor was found. Candidates are:"),
258+
candidates.stream().map(x -> String.format(" %s(%s) (Missing fields: %s)",
259+
x.getKey().getName(),
260+
Arrays.stream(x.getKey().getParameters()).map(p -> p.getName()).collect(joining(",")),
261+
new HashSet<>(x.getValue()))))
262+
.collect(joining(format("%n"))));
263+
}
264+
265+
return result;
266+
}
226267
}

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithConstructedField;
1717
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithGenericConstructor;
1818
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithPrivateConstructor;
19+
import com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithTwoConstructors;
1920
import org.junit.jupiter.api.DisplayName;
2021
import org.junit.jupiter.api.Nested;
2122
import org.junit.jupiter.api.Test;
@@ -49,12 +50,61 @@
4950
import static org.assertj.core.api.Assertions.assertThat;
5051
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
5152

53+
/*
54+
We use "arg0" as constructor argument-names, because .class files do not store formal parameter names by default.
55+
For records (Java 17+) proper names should work.
56+
*/
5257
class InstanceFactoryTest {
5358

5459
@Nested
5560
@DisplayName("when using constructor")
5661
class UsingConstructor {
5762

63+
@Test
64+
@DisplayName("throws exception for invalid customization")
65+
void throwsExceptionForInvalidCustomization() {
66+
var sut = new InstanceFactory(new SpecimenFactory(new Context(Configuration.configure())));
67+
68+
var type = new SpecimenType<TestObjectWithTwoConstructors>(){};
69+
var context = new CustomizationContext(List.of("arg0.nested", "missingExclusion"), Map.of(
70+
"arg1.nested", new Object(),
71+
"missingCustomization", new Object()), false);
72+
73+
assertThatExceptionOfType(SpecimenException.class)
74+
.isThrownBy(() -> sut.construct(type, context))
75+
.withMessageContaining("Cannot customize fields because no suitable constructor was found. Candidates are:\n")
76+
.withMessageContaining(" com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithTwoConstructors(arg0,arg1) (Missing fields: [missingCustomization, missingExclusion])\n")
77+
.withMessageContaining(" com.github.nylle.javafixture.testobjects.withconstructor.TestObjectWithTwoConstructors(arg0) (Missing fields: [missingCustomization, arg1, missingExclusion])");
78+
}
79+
80+
@Test
81+
@DisplayName("instance is created from suitable constructor for customized field")
82+
void canCreateInstanceFromSuitableConstructorForCustomizedField() {
83+
var sut = new InstanceFactory(new SpecimenFactory(new Context(Configuration.configure())));
84+
85+
var context = new CustomizationContext(List.of(), Map.of("arg1", "other"), false);
86+
87+
TestObjectWithTwoConstructors result = sut.construct(fromClass(TestObjectWithTwoConstructors.class), context);
88+
89+
assertThat(result).isInstanceOf(TestObjectWithTwoConstructors.class);
90+
assertThat(result.getValue()).isInstanceOf(String.class).isNotNull().isNotEqualTo("other");
91+
assertThat(result.getOther()).isEqualTo("other");
92+
}
93+
94+
@Test
95+
@DisplayName("instance is created from suitable constructor for omitted field")
96+
void canCreateInstanceFromSuitableConstructorForOmittedField() {
97+
var sut = new InstanceFactory(new SpecimenFactory(new Context(Configuration.configure())));
98+
99+
var context = new CustomizationContext(List.of("arg1"), Map.of(), false);
100+
101+
TestObjectWithTwoConstructors result = sut.construct(fromClass(TestObjectWithTwoConstructors.class), context);
102+
103+
assertThat(result).isInstanceOf(TestObjectWithTwoConstructors.class);
104+
assertThat(result.getValue()).isInstanceOf(String.class).isNotNull();
105+
assertThat(result.getOther()).isNull();
106+
}
107+
58108
@Test
59109
@DisplayName("instance is created from random constructor")
60110
void canCreateInstanceFromConstructor() {
@@ -163,7 +213,6 @@ void passExceptionToFallbackWhenConstructorThrows() {
163213
void argumentsCanBeCustomized() {
164214
var sut = new InstanceFactory(new SpecimenFactory(new Context(Configuration.configure())));
165215

166-
// use arg0, because .class files do not store formal parameter names by default
167216
var customizationContext = new CustomizationContext(List.of(), Map.of("arg0", "customized"), true);
168217
TestObject result = sut.construct(fromClass(TestObject.class), customizationContext);
169218

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.nylle.javafixture.testobjects.withconstructor;
2+
3+
public class TestObjectWithTwoConstructors {
4+
private String value;
5+
private String other;
6+
7+
public TestObjectWithTwoConstructors(String value, String other) {
8+
this.value = value;
9+
this.other = other;
10+
}
11+
12+
public TestObjectWithTwoConstructors(String value) {
13+
this.value = value;
14+
this.other = "default";
15+
}
16+
17+
public String getValue() {
18+
return value;
19+
}
20+
21+
public String getOther() {
22+
return other;
23+
}
24+
}

0 commit comments

Comments
 (0)