Skip to content

Commit dbb4d2c

Browse files
Allow stricter type check on compared object and their fields
1 parent 0574849 commit dbb4d2c

9 files changed

+311
-13
lines changed

src/main/java/org/assertj/core/api/recursive/comparison/ComparisonDifference.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import static java.lang.String.format;
1616
import static java.lang.String.join;
17+
import static org.assertj.core.util.Lists.list;
1718
import static org.assertj.core.util.Objects.areEqual;
1819

1920
import java.util.Collections;
@@ -50,6 +51,10 @@ public ComparisonDifference(List<String> path, Object actual, Object other, Stri
5051
this.additionalInformation = Optional.ofNullable(additionalInformation);
5152
}
5253

54+
public static ComparisonDifference rootComparisonDifference(Object actual, Object other, String additionalInformation) {
55+
return new ComparisonDifference(list(""), actual, other, additionalInformation);
56+
}
57+
5358
public String getPath() {
5459
return concatenatedPath;
5560
}

src/main/java/org/assertj/core/api/recursive/comparison/FieldComparators.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ public FieldComparators() {
4343
*
4444
* @param fieldLocation the FieldLocation where to apply the comparator
4545
* @param comparator the comparator it self
46-
* @param <T> the type of the objects for the comparator
4746
*/
48-
public <T> void registerComparator(FieldLocation fieldLocation, Comparator<? super T> comparator) {
47+
public void registerComparator(FieldLocation fieldLocation, Comparator<?> comparator) {
4948
fieldComparators.put(fieldLocation, comparator);
5049
}
5150

src/main/java/org/assertj/core/api/recursive/comparison/RecursiveComparisonConfiguration.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
public class RecursiveComparisonConfiguration {
3939

4040
public static final String INDENT_LEVEL_2 = " -";
41-
// private boolean strictTypeCheck = true;
41+
private boolean strictTypeChecking = false;
4242

4343
// fields to ignore section
4444
private boolean ignoreAllActualNullFields = false;
@@ -143,6 +143,14 @@ public void registerComparatorForField(FieldLocation fieldLocation, Comparator<?
143143
this.fieldComparators.registerComparator(fieldLocation, comparator);
144144
}
145145

146+
public boolean enforceStrictTypeChecking() {
147+
return strictTypeChecking;
148+
}
149+
150+
public void strictTypeChecking(boolean strictTypeChecking) {
151+
this.strictTypeChecking = strictTypeChecking;
152+
}
153+
146154
@Override
147155
public String toString() {
148156
return multiLineDescription(CONFIGURATION_PROVIDER.representation());
@@ -156,6 +164,7 @@ public String multiLineDescription(Representation representation) {
156164
describeOverriddenEqualsMethodsUsage(description, representation);
157165
describeRegisteredComparatorByTypes(description);
158166
describeRegisteredComparatorForFields(description);
167+
describeTypeCheckingStrictness(description);
159168
return description.toString();
160169
}
161170

@@ -308,4 +317,11 @@ private String formatRegisteredComparatorForField(Entry<FieldLocation, Comparato
308317
return format("%s %s -> %s%n", INDENT_LEVEL_2, comparatorForField.getKey().getFieldPath(), comparatorForField.getValue());
309318
}
310319

320+
private void describeTypeCheckingStrictness(StringBuilder description) {
321+
String str = strictTypeChecking
322+
? "- actual and expected objects and their fields were considered different when of incompatible types (i.e. expected type does not extend actual's type) even if all their fields match, for example a Person instance will never match a PersonDto (call strictTypeChecking(false) to change that behavior).%n"
323+
: "- actual and expected objects and their fields were compared field by field recursively even if they were not of the same type, this allows for example to compare a Person to a PersonDto (call strictTypeChecking(true) to change that behavior).%n";
324+
description.append(format(str));
325+
}
326+
311327
}

src/main/java/org/assertj/core/api/recursive/comparison/RecursiveComparisonDifferenceCalculator.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
package org.assertj.core.api.recursive.comparison;
1414

1515
import static java.lang.String.format;
16+
import static org.assertj.core.api.recursive.comparison.ComparisonDifference.rootComparisonDifference;
1617
import static org.assertj.core.internal.Objects.getDeclaredFieldsIncludingInherited;
18+
import static org.assertj.core.util.Lists.list;
1719
import static org.assertj.core.util.Sets.newHashSet;
1820
import static org.assertj.core.util.introspection.PropertyOrFieldSupport.COMPARISON;
1921

@@ -74,6 +76,9 @@ public class RecursiveComparisonDifferenceCalculator {
7476
*/
7577
public static List<ComparisonDifference> determineDifferences(Object actual, Object expected,
7678
RecursiveComparisonConfiguration recursiveComparisonConfiguration) {
79+
if (recursiveComparisonConfiguration.enforceStrictTypeChecking() && expectedTypeIsNotSubtypeOfActualType(actual, expected)) {
80+
return list(expectedAndActualTypeDifference(actual, expected));
81+
}
7782
return determineDifferences(actual, expected, null, recursiveComparisonConfiguration);
7883
}
7984

@@ -102,7 +107,7 @@ private static List<ComparisonDifference> determineDifferences(Object actual, Ob
102107
// fields were not the same according to the custom comparator
103108
differences.add(new ComparisonDifference(currentPath, key1, key2));
104109
}
105-
// since there was a custom comparator we don't need to inspect the fields
110+
// since there was a custom comparator we don't need to inspect the nested fields further
106111
continue;
107112
}
108113

@@ -155,7 +160,8 @@ private static List<ComparisonDifference> determineDifferences(Object actual, Ob
155160
// Handle all [] types. In order to be equal, the arrays must be the
156161
// same length, be of the same type, be in the same order, and all
157162
// elements within the array must be deeply equivalent.
158-
if (key1.getClass().isArray()) {
163+
Class<?> actualFieldClass = key1.getClass();
164+
if (actualFieldClass.isArray()) {
159165
if (!compareArrays(key1, key2, currentPath, toCompare, visited)) {
160166
differences.add(new ComparisonDifference(currentPath, key1, key2));
161167
continue;
@@ -214,24 +220,32 @@ private static List<ComparisonDifference> determineDifferences(Object actual, Ob
214220
}
215221

216222
if (!recursiveComparisonConfiguration.shouldIgnoreOverriddenEqualsOf(dualKey)
217-
&& hasCustomEquals(key1.getClass())) {
223+
&& hasCustomEquals(actualFieldClass)) {
218224
if (!key1.equals(key2)) {
219225
differences.add(new ComparisonDifference(currentPath, key1, key2));
220226
continue;
221227
}
222228
continue;
223229
}
224230

225-
Set<String> key1FieldsNames = getFieldsNames(getDeclaredFieldsIncludingInherited(key1.getClass()));
226-
Set<String> key2FieldsNames = getFieldsNames(getDeclaredFieldsIncludingInherited(key2.getClass()));
231+
Class<?> expectedFieldClass = key2.getClass();
232+
if (recursiveComparisonConfiguration.enforceStrictTypeChecking() && expectedTypeIsNotSubtypeOfActualType(dualKey)) {
233+
differences.add(new ComparisonDifference(currentPath, key1, key2,
234+
format("the fields are considered different since the comparison enforces strict type check and %s is not a subtype of %s",
235+
expectedFieldClass.getName(), actualFieldClass.getName())));
236+
continue;
237+
}
238+
239+
Set<String> key1FieldsNames = getFieldsNames(getDeclaredFieldsIncludingInherited(actualFieldClass));
240+
Set<String> key2FieldsNames = getFieldsNames(getDeclaredFieldsIncludingInherited(expectedFieldClass));
227241
if (!key2FieldsNames.containsAll(key1FieldsNames)) {
228242
Set<String> key1FieldsNamesNotInKey2 = newHashSet(key1FieldsNames);
229243
key1FieldsNamesNotInKey2.removeAll(key2FieldsNames);
230244
String missingFields = key1FieldsNamesNotInKey2.toString();
231-
String key2ClassName = key2.getClass().getName();
232-
String key1ClassName = key1.getClass().getName();
233-
String missingFieldsDescription = format(MISSING_FIELDS, key1ClassName, key2ClassName, key2.getClass().getSimpleName(),
234-
key1.getClass().getSimpleName(), missingFields);
245+
String key2ClassName = expectedFieldClass.getName();
246+
String key1ClassName = actualFieldClass.getName();
247+
String missingFieldsDescription = format(MISSING_FIELDS, key1ClassName, key2ClassName, expectedFieldClass.getSimpleName(),
248+
actualFieldClass.getSimpleName(), missingFields);
235249
differences.add(new ComparisonDifference(currentPath, key1, key2, missingFieldsDescription));
236250
} else {
237251
for (String fieldName : key1FieldsNames) {
@@ -660,4 +674,18 @@ private static boolean propertyOrFieldValuesAreEqual(Object actualFieldValue, Ob
660674
return org.assertj.core.util.Objects.areEqual(actualFieldValue, otherFieldValue);
661675
}
662676

677+
private static ComparisonDifference expectedAndActualTypeDifference(Object actual, Object expected) {
678+
String additionalInformation = format("actual and expected are considered different since the comparison enforces strict type check and expected type %s is not a subtype of actual type %s",
679+
expected.getClass().getName(), actual.getClass().getName());
680+
return rootComparisonDifference(actual, expected, additionalInformation);
681+
}
682+
683+
private static boolean expectedTypeIsNotSubtypeOfActualType(DualKey dualField) {
684+
return expectedTypeIsNotSubtypeOfActualType(dualField.key1, dualField.key2);
685+
}
686+
687+
private static boolean expectedTypeIsNotSubtypeOfActualType(Object actual, Object expected) {
688+
return !actual.getClass().isAssignableFrom(expected.getClass());
689+
}
690+
663691
}

src/test/java/org/assertj/core/api/recursive/comparison/RecursiveComparisonConfiguration_multiLineDescription_Test.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,24 @@ public void should_show_the_registered_comparator_for_specific_fields_alphabetic
138138
// @format:on
139139
}
140140

141+
@Test
142+
public void should_show_when_strict_type_checking_is_used() {
143+
// WHEN
144+
recursiveComparisonConfiguration.strictTypeChecking(true);
145+
String multiLineDescription = recursiveComparisonConfiguration.multiLineDescription(STANDARD_REPRESENTATION);
146+
// THEN
147+
assertThat(multiLineDescription).contains(format("- actual and expected objects and their fields were considered different when of incompatible types (i.e. expected type does not extend actual's type) even if all their fields match, for example a Person instance will never match a PersonDto (call strictTypeChecking(false) to change that behavior).%n"));
148+
}
149+
150+
@Test
151+
public void should_show_when_lenient_type_checking_is_used() {
152+
// WHEN
153+
recursiveComparisonConfiguration.strictTypeChecking(false);
154+
String multiLineDescription = recursiveComparisonConfiguration.multiLineDescription(STANDARD_REPRESENTATION);
155+
// THEN
156+
assertThat(multiLineDescription).contains(format("- actual and expected objects and their fields were compared field by field recursively even if they were not of the same type, this allows for example to compare a Person to a PersonDto (call strictTypeChecking(true) to change that behavior).%n"));
157+
}
158+
141159
@Test
142160
public void should_show_a_complete_multiline_description() {
143161
// GIVEN
@@ -171,7 +189,8 @@ public void should_show_a_complete_multiline_description() {
171189
"- these fields were compared with the following comparators:%n" +
172190
" - bar.baz -> AlwaysDifferentComparator%n" +
173191
" - foo -> AlwaysEqualComparator%n" +
174-
"- field comparators take precedence over type comparators.%n"));
192+
"- field comparators take precedence over type comparators.%n"+
193+
"- actual and expected objects and their fields were compared field by field recursively even if they were not of the same type, this allows for example to compare a Person to a PersonDto (call strictTypeChecking(true) to change that behavior).%n"));
175194
// @format:on
176195
}
177196

src/test/java/org/assertj/core/internal/objects/Objects_assertIsEqualToUsingRecursiveComparison_BaseTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,8 @@ void areEqualsByRecursiveComparison(Object actual, Object expected,
5050
static ComparisonDifference diff(String path, Object actual, Object other) {
5151
return new ComparisonDifference(list(path), actual, other);
5252
}
53+
54+
static ComparisonDifference diff(String path, Object actual, Object other, String additionalInformation) {
55+
return new ComparisonDifference(list(path), actual, other, additionalInformation);
56+
}
5357
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
3+
* the License. You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9+
* specific language governing permissions and limitations under the License.
10+
*
11+
* Copyright 2012-2018 the original author or authors.
12+
*/
13+
package org.assertj.core.internal.objects;
14+
15+
import static org.assertj.core.util.AssertionsUtil.expectAssertionError;
16+
17+
import java.util.Date;
18+
19+
import org.assertj.core.api.recursive.comparison.ComparisonDifference;
20+
import org.assertj.core.internal.objects.data.Giant;
21+
import org.assertj.core.internal.objects.data.Person;
22+
import org.assertj.core.internal.objects.data.PersonDto;
23+
import org.assertj.core.internal.objects.data.PersonDtoWithPersonNeighbour;
24+
import org.junit.jupiter.api.Test;
25+
26+
public class Objects_assertIsEqualToUsingRecursiveComparison_strictTypeCheck_Test
27+
extends Objects_assertIsEqualToUsingRecursiveComparison_BaseTest {
28+
29+
@Test
30+
public void should_pass_by_default_when_objects_data_are_equals_whatever_their_types_are() {
31+
// GIVEN
32+
Person actual = new Person("John");
33+
actual.home.address.number = 1;
34+
actual.dateOfBirth = new Date(123);
35+
actual.neighbour = new Person("Jack");
36+
actual.neighbour.home.address.number = 123;
37+
actual.neighbour.neighbour = new Person("James");
38+
actual.neighbour.neighbour.home.address.number = 124;
39+
40+
PersonDto expected = new PersonDto("John");
41+
expected.home.address.number = 1;
42+
expected.dateOfBirth = new Date(123);
43+
expected.neighbour = new PersonDto("Jack");
44+
expected.neighbour.home.address.number = 123;
45+
expected.neighbour.neighbour = new PersonDto("James");
46+
expected.neighbour.neighbour.home.address.number = 124;
47+
48+
// THEN
49+
areEqualsByRecursiveComparison(actual, expected, recursiveComparisonConfiguration);
50+
}
51+
52+
@Test
53+
public void should_pass_in_strict_type_check_mode_when_objects_data_are_equals_and_expected_type_is_compatible_with_actual_type() {
54+
// GIVEN
55+
Person actual = new Person("John");
56+
actual.home.address.number = 1;
57+
actual.dateOfBirth = new Date(123);
58+
actual.neighbour = new Person("Jack");
59+
actual.neighbour.home.address.number = 123;
60+
actual.neighbour.neighbour = new Person("James");
61+
actual.neighbour.neighbour.home.address.number = 124;
62+
63+
Giant expected = new Giant();
64+
expected.name = "John";
65+
expected.home.address.number = 1;
66+
expected.dateOfBirth = new Date(123);
67+
expected.neighbour = new Giant();
68+
expected.neighbour.name = "Jack";
69+
expected.neighbour.home.address.number = 123;
70+
expected.neighbour.neighbour = new Person("James");
71+
expected.neighbour.neighbour.home.address.number = 124;
72+
73+
Person expected2 = new Person("John");
74+
expected2.home.address.number = 1;
75+
expected2.dateOfBirth = new Date(123);
76+
expected2.neighbour = new Person("Jack");
77+
expected2.neighbour.home.address.number = 123;
78+
expected2.neighbour.neighbour = new Person("James");
79+
expected2.neighbour.neighbour.home.address.number = 124;
80+
81+
// WHEN
82+
recursiveComparisonConfiguration.strictTypeChecking(true);
83+
84+
// THEN
85+
areEqualsByRecursiveComparison(actual, expected, recursiveComparisonConfiguration);
86+
areEqualsByRecursiveComparison(actual, expected2, recursiveComparisonConfiguration);
87+
}
88+
89+
@Test
90+
public void should_fail_in_strict_type_checking_mode_when_actual_and_expected_have_the_same_data_but_incompatible_types() {
91+
// GIVEN
92+
Person actual = new Person("John");
93+
actual.home.address.number = 1;
94+
actual.dateOfBirth = new Date(123);
95+
actual.neighbour = new Person("Jack");
96+
actual.neighbour.home.address.number = 123;
97+
98+
PersonDtoWithPersonNeighbour expected = new PersonDtoWithPersonNeighbour("John");
99+
expected.home.address.number = 1;
100+
expected.dateOfBirth = new Date(123);
101+
expected.neighbour = new Person("Jack");
102+
expected.neighbour.home.address.number = 123;
103+
104+
recursiveComparisonConfiguration.strictTypeChecking(true);
105+
106+
// WHEN
107+
expectAssertionError(() -> areEqualsByRecursiveComparison(actual, expected, recursiveComparisonConfiguration));
108+
109+
// THEN
110+
// as neighbour comparison fails, the comparison skip any neighbour fields
111+
ComparisonDifference difference = diff("", actual, expected,
112+
"actual and expected are considered different since the comparison enforces strict type check and expected type org.assertj.core.internal.objects.data.PersonDtoWithPersonNeighbour is not a subtype of actual type org.assertj.core.internal.objects.data.Person");
113+
verifyShouldBeEqualByComparingFieldByFieldRecursivelyCall(actual, expected, difference);
114+
}
115+
116+
@Test
117+
public void should_fail_in_strict_type_checking_mode_when_actual_and_expected_fields_have_the_same_data_but_incompatible_types() {
118+
// GIVEN
119+
Something withA = new Something(new A(10));
120+
Something withB = new Something(new B(10));
121+
122+
recursiveComparisonConfiguration.strictTypeChecking(true);
123+
124+
// WHEN
125+
expectAssertionError(() -> areEqualsByRecursiveComparison(withA, withB, recursiveComparisonConfiguration));
126+
127+
// THEN
128+
// inner comparison fails as the fields have different types
129+
ComparisonDifference valueDifference = diff("inner", withA.inner, withB.inner,
130+
"the fields are considered different since the comparison enforces strict type check and org.assertj.core.internal.objects.Objects_assertIsEqualToUsingRecursiveComparison_strictTypeCheck_Test$B is not a subtype of org.assertj.core.internal.objects.Objects_assertIsEqualToUsingRecursiveComparison_strictTypeCheck_Test$A");
131+
verifyShouldBeEqualByComparingFieldByFieldRecursivelyCall(withA, withB, valueDifference);
132+
}
133+
134+
private static class Something {
135+
Inner inner; // can be A or B
136+
137+
public Something(Inner inner) {
138+
this.inner = inner;
139+
}
140+
}
141+
142+
private static class Inner {
143+
@SuppressWarnings("unused")
144+
int value;
145+
}
146+
147+
private static class A extends Inner {
148+
149+
public A(int value) {
150+
this.value = value;
151+
}
152+
153+
}
154+
155+
private static class B extends Inner {
156+
public B(int value) {
157+
this.value = value;
158+
}
159+
}
160+
161+
}

0 commit comments

Comments
 (0)