Skip to content

Commit

Permalink
Add Javadoc for API
Browse files Browse the repository at this point in the history
Refactor registering comparators by field or type
  • Loading branch information
joel-costigliola committed Feb 13, 2019
1 parent 7a76b57 commit f964272
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 150 deletions.
60 changes: 60 additions & 0 deletions src/main/java/org/assertj/core/api/AbstractObjectAssert.java
Expand Up @@ -26,6 +26,7 @@
import java.util.function.Function;
import java.util.stream.Stream;

import org.assertj.core.annotations.Beta;
import org.assertj.core.api.recursive.comparison.RecursiveComparisonAssert;
import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration;
import org.assertj.core.description.Description;
Expand Down Expand Up @@ -772,11 +773,70 @@ public <T> SELF returns(T expected, Function<ACTUAL, T> from) {
return myself;
}

/**
* Enable using a recursive field by field comparison strategy when calling the chained {@link RecursiveComparisonAssert#isEqualTo(Object) isEqualTo} assertion.
* <p>
* Example:
* <pre><code class='java'> public class Person {
* String name;
* double height;
* Home home = new Home();
* }
*
* public class Home {
* Address address = new Address();
* Date ownedSince;
* }
*
* public static class Address {
* int number;
* String street;
* }
*
* Person sherlock = new Person("Sherlock", 1.80);
* sherlock.home.ownedSince = new Date(123);
* sherlock.home.address.street = "Baker Street";
* sherlock.home.address.number = 221;
*
* Person sherlock2 = new Person("Sherlock", 1.80);
* sherlock2.home.ownedSince = new Date(123);
* sherlock2.home.address.street = "Baker Street";
* sherlock2.home.address.number = 221;
*
* // assertion succeeds as the data of both objects are the same.
* assertThat(sherlock).usingRecursiveComparison()
* .isEqualTo(sherlock2);
*
* // assertion fails because sherlock.equals(sherlock2) is false.
* assertThat(sherlock).isEqualTo(sherlock2);</code></pre>
* <p>
* The recursive comparison is performed according to the default {@link RecursiveComparisonConfiguration} that is:
* <ul>
* <li>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 {@link RecursiveComparisonAssert#withStrictTypeChecking() withStrictTypeChecking()} to change that behavior). </li>
* <li>overridden equals methods were used in the comparison </li>
* <li>these types were compared with the following comparators: </li>
* <ul>
* <li>java.lang.Double -> DoubleComparator[precision=1.0E-15] </li>
* <li>java.lang.Float -> FloatComparator[precision=1.0E-6] </li>
* </ul>
* </ul>
*
* @return a new {@link RecursiveComparisonAssert} instance
*/
@Beta
public RecursiveComparisonAssert usingRecursiveComparison() {
return usingRecursiveComparison(new RecursiveComparisonConfiguration());
}

// TODO soft assertion tests: add to method changing the object under tests

/**
* Same as {@link #usingRecursiveComparison()} but allows to specify your own {@link RecursiveComparisonConfiguration}.
* @param recursiveComparisonConfiguration the {@link RecursiveComparisonConfiguration} used in the chained {@link RecursiveComparisonAssert#isEqualTo(Object) isEqualTo} assertion.
*
* @return a new {@link RecursiveComparisonAssert} instance built with the given {@link RecursiveComparisonConfiguration}.
*/
@Beta
public RecursiveComparisonAssert usingRecursiveComparison(RecursiveComparisonConfiguration recursiveComparisonConfiguration) {
return new RecursiveComparisonAssert(actual, recursiveComparisonConfiguration);
}
Expand Down
Expand Up @@ -12,13 +12,11 @@
*/
package org.assertj.core.api.recursive.comparison;

import static org.assertj.core.api.recursive.comparison.FieldLocation.fielLocation;
import static org.assertj.core.error.ShouldBeEqualByComparingFieldByFieldRecursively.shouldBeEqualByComparingFieldByFieldRecursively;

import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.assertj.core.api.WritableAssertionInfo;
Expand Down Expand Up @@ -62,16 +60,25 @@ public RecursiveComparisonAssert as(String description, Object... args) {

/**
* Asserts that the object under test (actual) is equal to the given object when compared field by field recursively (including
* inherited fields are included in the comparison).If the comparison fails it will report all the differences found and which
* inherited fields are included in the comparison). If the comparison fails it will report all the differences found and which
* effective {@link RecursiveComparisonConfiguration} was used to help users understand the failure.
* TODO add link to assertj website documentation
* <p>
* This is typically useful when actual's {@code equals} was not overridden.
* <p>
* The comparison is <b>not symmetrical</b> since it is limited to actual's fields, the algorithm firts gather all actual's fields
* The comparison is <b>not symmetrical</b> since it is <b>limited to actual's fields</b>, the algorithm gather all actual's fields
* and then compare them to the corresponding expected's fields.
* It is then possible for the expected object to have more fields than actual which is handy when comparing a base type to a subtype.
* <p>
* <strong>Strict/lenient recursive comparison</strong>
* <p>
* By default the objects to compare can be of different types but must have the same properties/fields. For example if object under test has a {@code work} field of type {@code Address},
* the expected object to compare the object under test to must also have one but it can of a different type like {@code AddressDto}.
* <p>
* It is possible to enforce strict type checking by calling {@link #withStrictTypeChecking()} and make the comparison fail whenever the compared objects or their fields are not compatible.<br>
* Compatible means that the expected object/field types are the same or a subtype of actual/field types, for example if actual is an {@code Animal} and expected a {@code Dog}, they will be compared fiels by field in strict type checking mode.
* <p>
* <p>
* <strong>Ignoring null fields in the recursive comparison</strong>
* <p>
* When an object is partially populated, it can still be interesting to see if its populated values are correct against a fully populated object.
Expand All @@ -81,73 +88,66 @@ public RecursiveComparisonAssert as(String description, Object... args) {
* <p>
* <strong>Recursive comparison use of overridden {@code equals} methods</strong>
* <p>
* By default the recursive comparison is <b>not</b> applied on fields whose classes have an overridden {@code equals} implementation,
* concretely it means {@code equals} is used to compare these fields instead of keeing on applying the recursive comparison.
* By default the recursive comparison is <b>not</b> applied on fields whose classes have overridden the {@code equals} method,
* concretely it means {@code equals} is used to compare these fields instead of keeping on applying the recursive comparison.
* The rationale is that if a class has redefined {@code equals} then it should be used to compare instances unless having a good reason.
* <p>
* It is possible though to change this behavior and force recursive comparison by calling any of these methods (but before calling {@code isEqualTo} otherwise this has no effect!):
* <ol>
* <li> {@link #ignoringOverriddenEqualsForTypes(Class...)} Any fields of these classes are compared recursively</li>
* <li> {@link #ignoringOverriddenEqualsForFields(String...)} Any given fields are compared recursively</li>
* <li> {@link #ignoringOverriddenEqualsForFieldsMatchingRegexes(String...)} Any fields matching one of these regexes are compared recursively</li>
* <li> {@link #ignoringAllOverriddenEquals()} except for basic types (TODO define basic types), all fields are compared field by field recursively.</li>
* </ol>
* <strong>Recursive comparison and cycles</strong>
* <p>
* The recursive comparison handles cycles. By default {@code floats} are compared with a precision of 1.0E-6 and {@code doubles} with 1.0E-15.
* The recursive comparison handles cycles.
* <p>
* You can specify a custom comparator per (nested) fields or type with respectively {@code #usingComparatorForFields(Comparator, String...) usingComparatorForFields(Comparator, String...)}
* and {@code #usingComparatorForType(Comparator, Class)}.
* <strong>Comparator used in the recursive comparison</strong>
* <p>
* The objects to compare can be of different types but must have the same properties/fields. For example if actual object has a name String field, it is expected the other object to also have one.
* If an object has a field and a property with the same name, the property value will be used over the field.
* By default {@code floats} are compared with a precision of 1.0E-6 and {@code doubles} with 1.0E-15.
* <p>
* Example:
* You can specify a custom comparator per (nested) fields or type with the methods below (but before calling {@code isEqualTo} otherwise this has no effect!):
* <ol>
* <li> {@link #withComparatorForFields(Comparator, String...) withComparatorForFields(Comparator, String...)} for one or multiple fields</li>
* <li> {@link #withComparatorForType(Comparator, Class)} for a given type</li>
* </ol>
* <p>
* Note that field comparators always take precedence over type comparators.
* <p>
* <strong>Example</strong>
* <p>
* Here is a basic example with a default {@link RecursiveComparisonConfiguration}, you can find other examples for each of the method changing the recursive comparison behavior
* like {@link #ignoringFields(String...)}.
* <pre><code class='java'> public class Person {
* public String name;
* public double height;
* public Home home = new Home();
* public Person bestFriend;
* // constructor with name and height omitted for brevity
* String name;
* double height;
* Home home = new Home();
* }
*
* public class Home {
* public Address address = new Address();
* Address address = new Address();
* Date ownedSince;
* }
*
* public static class Address {
* public int number = 1;
* int number;
* String street;
* }
*
* Person jack = new Person("Jack", 1.80);
* jack.home.address.number = 123;
*
* Person jackClone = new Person("Jack", 1.80);
* jackClone.home.address.number = 123;
*
* // cycle are handled in comparison
* jack.bestFriend = jackClone;
* jackClone.bestFriend = jack;
*
* // will fail as equals compares object references
* assertThat(jack).isEqualTo(jackClone);
*
* // jack and jackClone are equals when doing a recursive field by field comparison
* assertThat(jack).isEqualToComparingFieldByFieldRecursively(jackClone);
*
* // any type/field can be compared with a a specific comparator.
* // let's change jack's height a little bit
* jack.height = 1.81;
*
* // assertion fails because of the height difference
* // (the default precision comparison for double is 1.0E-15)
* assertThat(jack).isEqualToComparingFieldByFieldRecursively(jackClone);
* Person sherlock = new Person("Sherlock", 1.80);
* sherlock.home.ownedSince = new Date(123);
* sherlock.home.address.street = "Baker Street";
* sherlock.home.address.number = 221;
*
* // this succeeds because we allow a 0.5 tolerance on double
* assertThat(jack).usingComparatorForType(new DoubleComparator(0.5), Double.class)
* .isEqualToComparingFieldByFieldRecursively(jackClone);
* Person sherlock2 = new Person("Sherlock", 1.80);
* sherlock2.home.ownedSince = new Date(123);
* sherlock2.home.address.street = "Baker Street";
* sherlock2.home.address.number = 221;
*
* // you can set a comparator on specific fields (nested fields are supported)
* assertThat(jack).usingComparatorForFields(new DoubleComparator(0.5), "height")
* .isEqualToComparingFieldByFieldRecursively(jackClone);</code></pre>
* // assertion succeeds as the data of both objects are the same.
* assertThat(sherlock).usingRecursiveComparison()
* .isEqualTo(sherlock2);</code></pre>
*
* @param expected the object to compare {@code actual} to.
* @return {@code this} assertion object.
Expand Down Expand Up @@ -183,8 +183,6 @@ public RecursiveComparisonAssert isEqualTo(Object expected) {
* String name;
* double height;
* Home home = new Home();
* Person bestFriend;
* // constructor omitted for brevity
* }
*
* public class Home {
Expand Down Expand Up @@ -323,7 +321,7 @@ public RecursiveComparisonAssert ignoringFieldsMatchingRegexes(String... regexes
* - at some point we need to compare something!
* <p>
* For the recursive comparison to use the overridden {@code equals} of a given type anyway (like {@link Date}) you can register
* a type comparator using {@link #withComparatorForType(Class, Comparator)}.
* a type comparator using {@link #withComparatorForType(Comparator, Class)}.
* <p>
* TODO introduce {@code ignoringAllOverriddenEqualsExceptFor(Class<?>... classes)} ?
* <p>
Expand Down Expand Up @@ -612,32 +610,86 @@ public RecursiveComparisonAssert withStrictTypeChecking() {
return this;
}

/**
* Allows to register a specific comparator to compare fields with the given locations.
* A typical usage is for comparing double/float fields with a given precision.
* <p>
* Comparators specified by this method have precedence over comparators added with {@link #withComparatorForType(Comparator, Class)}.
* <p>
* The field locations must be specified from the root object,
* for example if {@code Foo} has a {@code Bar} field which has an {@code id}, one can register to a comparator for Bar's {@code id} by calling:
* <pre><code class='java'> withComparatorForField("bar.id", idComparator)</code></pre>
* <p>
* Complete example:
* <pre><code class='java'> public class TolkienCharacter {
* String name;
* double height;
* }
*
* TolkienCharacter frodo = new TolkienCharacter(&quot;Frodo&quot;, 1.2);
* TolkienCharacter tallerFrodo = new TolkienCharacter(&quot;Frodo&quot;, 1.3);
* TolkienCharacter reallyTallFrodo = new TolkienCharacter(&quot;Frodo&quot;, 1.9);
*
* Comparator&lt;Double&gt; closeEnough = (d1, d2) -&gt; Math.abs(d1 - d2) &lt;= 0.5 ? 0 : 1;
*
* // assertions succeed
* assertThat(frodo).usingRecursiveComparison()
* .withComparatorForFields(closeEnough, &quot;height&quot;)
* .isEqualTo(tallerFrodo);
*
* // assertion fails
* assertThat(frodo).usingRecursiveComparison()
* .withComparatorForFields(closeEnough, &quot;height&quot;)
* .isEqualTo(reallyTallFrodo);</code></pre>
*
* @param comparator the {@link java.util.Comparator Comparator} to use to compare the given fields
* @param fieldLocations the location from the root object of the fields the comparator should be used for
* @return this {@link RecursiveComparisonAssert} to chain other methods.
*/
@CheckReturnValue
public RecursiveComparisonAssert withComparatorForField(String fieldLocation, Comparator<?> comparator) {
recursiveComparisonConfiguration.registerComparatorForField(fielLocation(fieldLocation), comparator);
return this;
}

@SafeVarargs
@CheckReturnValue
public final RecursiveComparisonAssert withComparatorForFields(Map.Entry<String, Comparator<?>>... comparatorByFields) {
Stream.of(comparatorByFields).forEach(this::withComparatorForField);
return this;
}

@CheckReturnValue
public <T> RecursiveComparisonAssert withComparatorForType(Class<T> type, Comparator<? super T> comparator) {
recursiveComparisonConfiguration.registerComparatorForType(type, comparator);
public RecursiveComparisonAssert withComparatorForFields(Comparator<?> comparator, String... fieldLocations) {
Stream.of(fieldLocations)
.map(FieldLocation::new)
.forEach(fieldLocation -> recursiveComparisonConfiguration.registerComparatorForField(comparator, fieldLocation));
return this;
}

// can't type Comparator/Class with <T> since each entry is about different types (no reason to be all related to T)
@SuppressWarnings({ "rawtypes", "unchecked" })
@SafeVarargs
/**
* Allows to register a specific comparator to compare the fields with the given type.
* A typical usage is for comparing double/float fields with a given precision.
* <p>
* Comparators specified by this method have less precedence than comparators added with {@link #withComparatorForFields(Comparator, String...) withComparatorForFields(Comparator, String...)}.
* <p>
* Example:
* <pre><code class='java'> public class TolkienCharacter {
* String name;
* double height;
* }
*
* TolkienCharacter frodo = new TolkienCharacter(&quot;Frodo&quot;, 1.2);
* TolkienCharacter tallerFrodo = new TolkienCharacter(&quot;Frodo&quot;, 1.3);
* TolkienCharacter reallyTallFrodo = new TolkienCharacter(&quot;Frodo&quot;, 1.9);
*
* Comparator&lt;Double&gt; closeEnough = (d1, d2) -&gt; Math.abs(d1 - d2) &lt;= 0.5 ? 0 : 1;
*
* // assertions succeed
* assertThat(frodo).usingRecursiveComparison()
* .withComparatorForType(closeEnough, Double.class)
* .isEqualTo(tallerFrodo);
*
* // assertion fails
* assertThat(frodo).usingRecursiveComparison()
* .withComparatorForType(closeEnough, Double.class)
* .isEqualTo(reallyTallFrodo);</code></pre>
*
* @param comparator the {@link java.util.Comparator Comparator} to use to compare the given fields
* @param type the type to be compared with the given comparator.
*
* @return this {@link RecursiveComparisonAssert} to chain other methods.
*/
@CheckReturnValue
public final RecursiveComparisonAssert withComparatorForTypes(Map.Entry<Class, Comparator>... comparatorByTypes) {
Stream.of(comparatorByTypes)
.forEach(comparatorByType -> withComparatorForType(comparatorByType.getKey(), comparatorByType.getValue()));
public <T> RecursiveComparisonAssert withComparatorForType(Comparator<? super T> comparator, Class<T> type) {
recursiveComparisonConfiguration.registerComparatorForType(comparator, type);
return this;
}

Expand All @@ -650,9 +702,4 @@ private List<ComparisonDifference> determineDifferencesWith(Object expected) {
return recursiveComparisonDifferenceCalculator.determineDifferences(actual, expected, recursiveComparisonConfiguration);
}

// syntactic sugar
private RecursiveComparisonAssert withComparatorForField(Map.Entry<String, Comparator<?>> comparatorByField) {
return withComparatorForField(comparatorByField.getKey(), comparatorByField.getValue());
}

}

0 comments on commit f964272

Please sign in to comment.