Repository: assertj/assertj (Java)

Issue:
#isEqualTo(Object other) takes invalid shortcut
#### Summary
Before PR #1767 (specifically commit [76762a3664b3af2101d580355a05c6f0b3aa46cc](../commit/76762a3664b3af2101d580355a05c6f0b3aa46cc)) an object x was not automatically considered equal to itself when using `BDDAssertions.assertThat(x).isEqualTo(x);`
By switching from 

```java
    if (o1 == null) {
      return o2 == null;
    }
    if (o1.equals(o2)) {
      return true;
    }
```
to calling `java.util.Objects.deepEquals(o1, o2);` we end up with
```java
    if (o1 == o2)
        return true;
    else if (o1 == null || o2 == null)
        return false;
    else
        // defaults to o1.equals(o2) if type is not an array
        return Arrays.deepEquals0(o1, 2);
```

This means when testing a custom equals implementation of a class, BDDAssertions doesn't actually do what we expect it to do and we may oversee bugs in the application code.

#### Example

```java
// fails with assertj 3.16.1; succeeds with 3.13.2; probably broken since 13.15.1
@Test
void testEquals() {
	Object x = new Object() {
		@Override
		public boolean equals(Object other) {
			return false;
		}
	};
	BDDAssertions.assertThat(x).isNotEqualTo(x);
}
```



Hints:
Thanks for the detailed explanation here and in #1948, I will look into them.
> Before PR #1767 (specifically commit [76762a3664b3af2101d580355a05c6f0b3aa46cc](../commit/76762a3664b3af2101d580355a05c6f0b3aa46cc)) an object x was not automatically considered equal to itself when using `BDDAssertions.assertThat(x).isEqualTo(x);`

Right and I think the previous behavior was wrong because it could violate [the contract](https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-) of `equals`, where it is mentioned:
> for any non-null reference value x, x.equals(x) should return true

The custom `equals` in the example is also violating that requirement.

Honestly, I would not restore the previous behavior. @joel-costigliola what do you think?
I do agree with @scordio, it is very difficult to argue that given `o1 == o2` then `o1.equals(o2)` could return `false`.
I don't think the previous behaviour intended to allow that (might date from the Fest Assert library AssertJ is based upon).

If you want to have total control over `isEqualTo`, you can register a comparator:
```
// fails as isEqualTo checks is: comparator.compare(actual, other) == 0
assertThat(frodo).usingComparator((o1, o2) -> 1)
                 .isEqualTo(frodo);
```

Anyway @nioertel thanks for the detailed issue, appreciated! 
I'm curious what was the exact use case that led you to raise this issue? Maybe there is something we can do.
I disagree.
AssertJ should not make any assumptions about the code under test regarding compliance with any contracts from the JDK docs. It should give the developer a chance to test whether the code under test fulfils these contracts.
Until this change, assertThat(a).isEqualTo(b) for single objects behaved exactly like a.equals(b), except that it added null-safety on top. In my opinion this was the correct way, as explained above.

Niels
> It should give the developer a chance to test whether the code under test fulfils these contracts.

I missed this point in the issue description. Do I understand correctly that you would like to use `isEqualTo` to test if the `equals` implementation fulfills the contract? I would suggest to use [EqualsVerifier](https://jqno.nl/equalsverifier/) for this purpose. AssertJ might help with that, but supporting `equals` testing was never the main target.

Anyway, playing a bit more with this topic and the `equals` implementation above, another way to see the issue is with the following example:

```java
assertThat(x.equals(x)).isFalse(); // succeeds
assertThat(x).isNotEqualTo(x); // fails
```

More specifically:

```java
x.equals(x); // false
java.util.Objects.equals(x, x); // true
java.util.Objects.deepEquals(x, x); // true
```

The shortcut is in the `java.util.Objects.equals` implementation:

```java
    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
```
which states in the javadoc:

> Returns true if the arguments are equal to each other and false otherwise. Consequently, if both arguments are null, true is returned and if exactly one argument is null, false is returned. Otherwise, **equality is determined by using the equals method of the first argument**.

but the sentence in bold is actually in contrast with the result above.

Same for `deepEquals`. We could argue that this is a JDK issue.

I am still not sure how to proceed.
There is no obvious answer here, either AssertJ `isEqualTo` fully sticks to `equals` semantics and would not perform null check or reference checks or it performs them and the array comparison ones.

I would keep the current behaviour for now until we are presented a real life use case or more arguments in favor of the old behaviour.

The current behaviour does not prevent alternatives approaches like EqualVerifier or a simple `assertThat(x.equals(x)).isFalse()`;

Let's leave that issue open for the sake of discussion, happy to reevaluate our position in light of new arguments.
This topic has been also discussed in [JDK-8196069](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8196069).
Hello maintainers.

#### Summary

Breaking change detected in code coverage. Self-reference check in equals() not recognized by `isEqualTo(..)` which leads to less code coverage. 

Works with AssertJ 3.14.0
Not working with AssertJ 3.18.1

#### Example

```java
class EqualToTest {

    @Test
    void test() {
        final var myObj = new MyObject();
        assertThat(myObj).isEqualTo(myObj);
    }

    private static class MyObject {
        @Override
        public boolean equals(Object obj) {
            // simplified check for demo purposes
            return this == obj;
        }
    }

}
```

Example with AssertJ 3.14:
<img width="474" alt="assertj-3 14" src="https://user-images.githubusercontent.com/25742159/105613034-3937e300-5dc0-11eb-9da6-5297c6ebe37c.png">

Example with AssertJ 3.18.1:
<img width="474" alt="assertj-3 18 1" src="https://user-images.githubusercontent.com/25742159/105613039-4228b480-5dc0-11eb-9470-f8365467d928.png">

Hope that helps and hope it will be fixed soon and there won't be a forced need for a workaround using an extra dependency like EqualsVerifier as AssertJ should recover the most important parts OOTB 👍 
@pitschr AssertJ `isEqualTo` is not designed to provide full coverage for custom equals implementations. We usually suggest EqualsVerifier to achieve this.

A similar example has been discussed in #2025.

To be noted that only the reference check is not covered with the custom `equals` invocation. This is due to `Objects.deepEquals()` which takes care of that.
Yeah, EqualsVerifier is a good library. I agree that the EqualsVerifier is superior when we need to check `equals` and `hashCode`. But I still think that AssertJ should call the real `equals` method even if self-reference to avoid unexpected behavior.

The missing coverage through `isEqualTo(..)` was just accidentally discovered. Now I am in the situation that I am not sure if there might be other methods/scenarios which are "not designed" for AssertJ. Especially when it was covered before.

It is good to have it clarified that this drawback was done consciously. I still hope that AssertJ can be improved in this area in future.

Patch:
diff --git a/src/main/java/org/assertj/core/internal/AtomicReferenceArrayElementComparisonStrategy.java b/src/main/java/org/assertj/core/internal/AtomicReferenceArrayElementComparisonStrategy.java
--- a/src/main/java/org/assertj/core/internal/AtomicReferenceArrayElementComparisonStrategy.java
+++ b/src/main/java/org/assertj/core/internal/AtomicReferenceArrayElementComparisonStrategy.java
@@ -20,7 +20,7 @@
 
 public class AtomicReferenceArrayElementComparisonStrategy<T> extends StandardComparisonStrategy {
 
-  private Comparator<? super T> elementComparator;
+  private final Comparator<? super T> elementComparator;
 
   public AtomicReferenceArrayElementComparisonStrategy(Comparator<? super T> elementComparator) {
     this.elementComparator = elementComparator;
diff --git a/src/main/java/org/assertj/core/internal/IterableElementComparisonStrategy.java b/src/main/java/org/assertj/core/internal/IterableElementComparisonStrategy.java
--- a/src/main/java/org/assertj/core/internal/IterableElementComparisonStrategy.java
+++ b/src/main/java/org/assertj/core/internal/IterableElementComparisonStrategy.java
@@ -21,7 +21,7 @@
 
 public class IterableElementComparisonStrategy<T> extends StandardComparisonStrategy {
 
-  private Comparator<? super T> elementComparator;
+  private final Comparator<? super T> elementComparator;
 
   public IterableElementComparisonStrategy(Comparator<? super T> elementComparator) {
     this.elementComparator = elementComparator;
diff --git a/src/main/java/org/assertj/core/internal/ObjectArrayElementComparisonStrategy.java b/src/main/java/org/assertj/core/internal/ObjectArrayElementComparisonStrategy.java
--- a/src/main/java/org/assertj/core/internal/ObjectArrayElementComparisonStrategy.java
+++ b/src/main/java/org/assertj/core/internal/ObjectArrayElementComparisonStrategy.java
@@ -19,7 +19,7 @@
 
 public class ObjectArrayElementComparisonStrategy<T> extends StandardComparisonStrategy {
 
-  private Comparator<? super T> elementComparator;
+  private final Comparator<? super T> elementComparator;
 
   public ObjectArrayElementComparisonStrategy(Comparator<? super T> elementComparator) {
 	this.elementComparator = elementComparator;
diff --git a/src/main/java/org/assertj/core/internal/StandardComparisonStrategy.java b/src/main/java/org/assertj/core/internal/StandardComparisonStrategy.java
--- a/src/main/java/org/assertj/core/internal/StandardComparisonStrategy.java
+++ b/src/main/java/org/assertj/core/internal/StandardComparisonStrategy.java
@@ -66,15 +66,42 @@ public String asText() {
   }
 
   /**
-   * Returns true if actual and other are equal based on {@link java.util.Objects#deepEquals(Object, Object)}, false otherwise.
+   * Returns {@code true} if the arguments are deeply equal to each other, {@code false} otherwise.
+   * <p>
+   * It mimics the behavior of {@link java.util.Objects#deepEquals(Object, Object)}, but without performing a reference
+   * check between the two arguments. According to {@code deepEquals} javadoc, the reference check should be delegated
+   * to the {@link Object#equals equals} method of the first argument, but this is not happening. Bug JDK-8196069 also
+   * mentions this gap.
+   *
+   * @param actual the object to compare to {@code other}
+   * @param other the object to compare to {@code actual}
+   * @return {@code true} if the arguments are deeply equal to each other, {@code false} otherwise
+   *
+   * @see <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8196069">JDK-8196069</a>
    *
-   * @param actual the object to compare to other
-   * @param other the object to compare to actual
-   * @return true if actual and other are equal based on {@link java.util.Objects#deepEquals(Object, Object)}, false otherwise.
    */
   @Override
   public boolean areEqual(Object actual, Object other) {
-    return java.util.Objects.deepEquals(actual, other);
+    if (actual == null) return other == null;
+    if (actual instanceof Object[] && other instanceof Object[])
+      return java.util.Arrays.deepEquals((Object[]) actual, (Object[]) other);
+    if (actual instanceof byte[] && other instanceof byte[])
+      return java.util.Arrays.equals((byte[]) actual, (byte[]) other);
+    if (actual instanceof short[] && other instanceof short[])
+      return java.util.Arrays.equals((short[]) actual, (short[]) other);
+    if (actual instanceof int[] && other instanceof int[])
+      return java.util.Arrays.equals((int[]) actual, (int[]) other);
+    if (actual instanceof long[] && other instanceof long[])
+      return java.util.Arrays.equals((long[]) actual, (long[]) other);
+    if (actual instanceof char[] && other instanceof char[])
+      return java.util.Arrays.equals((char[]) actual, (char[]) other);
+    if (actual instanceof float[] && other instanceof float[])
+      return java.util.Arrays.equals((float[]) actual, (float[]) other);
+    if (actual instanceof double[] && other instanceof double[])
+      return java.util.Arrays.equals((double[]) actual, (double[]) other);
+    if (actual instanceof boolean[] && other instanceof boolean[])
+      return java.util.Arrays.equals((boolean[]) actual, (boolean[]) other);
+    return actual.equals(other);
   }
 
   /**


Test patch:
diff --git a/src/test/java/org/assertj/core/internal/StandardComparisonStrategy_areEqual_Test.java b/src/test/java/org/assertj/core/internal/StandardComparisonStrategy_areEqual_Test.java
--- a/src/test/java/org/assertj/core/internal/StandardComparisonStrategy_areEqual_Test.java
+++ b/src/test/java/org/assertj/core/internal/StandardComparisonStrategy_areEqual_Test.java
@@ -12,78 +12,409 @@
  */
 package org.assertj.core.internal;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
 
-import org.assertj.core.util.Objects;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 /**
- * Tests for {@link StandardComparisonStrategy#areEqual(Object, Object)}.<br>
- * Conceptually the same as {@link Objects#areEqual(Object, Object)} but I don't know how to verify/test that
- * {@link StandardComparisonStrategy#areEqual(Object, Object)} simply calls {@link Objects#areEqual(Object, Object)}
+ * Tests for {@link StandardComparisonStrategy#areEqual(Object, Object)}.
  * 
  * @author Joel Costigliola
  */
+@DisplayName("StandardComparisonStrategy areEqual")
 class StandardComparisonStrategy_areEqual_Test {
 
-  private static StandardComparisonStrategy standardComparisonStrategy = StandardComparisonStrategy.instance();
+  private final StandardComparisonStrategy underTest = StandardComparisonStrategy.instance();
+
+  @Test
+  void should_return_true_if_both_actual_and_other_are_null() {
+    // WHEN
+    boolean result = underTest.areEqual(null, null);
+    // THEN
+    then(result).isTrue();
+  }
+
+  @Test
+  void should_return_false_if_actual_is_null_and_other_is_not() {
+    // GIVEN
+    Object other = new Object();
+    // WHEN
+    boolean result = underTest.areEqual(null, other);
+    // THEN
+    then(result).isFalse();
+  }
+
+  @Test
+  void should_return_true_if_Object_arrays_are_equal() {
+    // GIVEN
+    Object[] actual = { "Luke", "Yoda", "Leia" };
+    Object[] other = { "Luke", "Yoda", "Leia" };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
+  }
+
+  @Test
+  void should_return_false_if_Object_arrays_are_not_equal() {
+    // GIVEN
+    Object[] actual = { "Luke", "Leia", "Yoda" };
+    Object[] other = { "Luke", "Yoda", "Leia" };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
+  }
+
+  @Test
+  void should_return_true_if_byte_arrays_are_equal() {
+    // GIVEN
+    byte[] actual = { 1, 2, 3 };
+    byte[] other = { 1, 2, 3 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
+  }
+
+  @Test
+  void should_return_false_if_byte_arrays_are_not_equal() {
+    // GIVEN
+    byte[] actual = { 1, 2, 3 };
+    byte[] other = { 4, 5, 6 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
+  }
+
+  @Test
+  void should_return_true_if_short_arrays_are_equal() {
+    // GIVEN
+    short[] actual = { 1, 2, 3 };
+    short[] other = { 1, 2, 3 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
+  }
 
   @Test
-  void should_return_true_if_both_Objects_are_null_with_verify() {
-    assertThat(standardComparisonStrategy.areEqual(null, null)).isTrue();
+  void should_return_false_if_short_arrays_are_not_equal() {
+    // GIVEN
+    short[] actual = { 1, 2, 3 };
+    short[] other = { 4, 5, 6 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
   }
 
   @Test
-  void should_return_true_if_both_Objects_are_null() {
-    assertThat(standardComparisonStrategy.areEqual(null, null)).isTrue();
+  void should_return_true_if_int_arrays_are_equal() {
+    // GIVEN
+    int[] actual = { 1, 2, 3 };
+    int[] other = { 1, 2, 3 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
   }
 
   @Test
-  void should_return_true_if_Objects_are_equal() {
-    assertThat(standardComparisonStrategy.areEqual("Yoda", "Yoda")).isTrue();
+  void should_return_false_if_int_arrays_are_not_equal() {
+    // GIVEN
+    int[] actual = { 1, 2, 3 };
+    int[] other = { 4, 5, 6 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
   }
 
   @Test
-  void should_return_are_not_equal_if_first_Object_is_null_and_second_is_not() {
-    assertThat(standardComparisonStrategy.areEqual(null, "Yoda")).isFalse();
+  void should_return_true_if_long_arrays_are_equal() {
+    // GIVEN
+    long[] actual = { 1L, 2L, 3L };
+    long[] other = { 1L, 2L, 3L };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
   }
 
   @Test
-  void should_return_are_not_equal_if_second_Object_is_null_and_first_is_not() {
-    assertThat(standardComparisonStrategy.areEqual("Yoda", null)).isFalse();
+  void should_return_false_if_long_arrays_are_not_equal() {
+    // GIVEN
+    long[] actual = { 1L, 2L, 3L };
+    long[] other = { 4L, 5L, 6L };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
   }
 
   @Test
-  void should_return_are_not_equal_if_Objects_are_not_equal() {
-    assertThat(standardComparisonStrategy.areEqual("Yoda", 2)).isFalse();
+  void should_return_true_if_char_arrays_are_equal() {
+    // GIVEN
+    char[] actual = { '1', '2', '3' };
+    char[] other = { '1', '2', '3' };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
   }
 
   @Test
-  void should_return_true_if_arrays_of_Objects_are_equal() {
-    Object[] a1 = { "Luke", "Yoda", "Leia" };
-    Object[] a2 = { "Luke", "Yoda", "Leia" };
-    assertThat(standardComparisonStrategy.areEqual(a1, a2)).isTrue();
+  void should_return_false_if_char_arrays_are_not_equal() {
+    // GIVEN
+    char[] actual = { '1', '2', '3' };
+    char[] other = { '4', '5', '6' };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
   }
 
   @Test
-  void should_return_true_if_arrays_of_primitives_are_equal() {
-    int[] a1 = { 6, 8, 10 };
-    int[] a2 = { 6, 8, 10 };
-    assertThat(standardComparisonStrategy.areEqual(a1, a2)).isTrue();
+  void should_return_true_if_float_arrays_are_equal() {
+    // GIVEN
+    float[] actual = { 1.0f, 2.0f, 3.0f };
+    float[] other = { 1.0f, 2.0f, 3.0f };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
   }
 
   @Test
-  void should_return_false_if_arrays_of_Objects_are_not_equal() {
-    Object[] a1 = { "Luke", "Yoda", "Leia" };
-    Object[] a2 = new Object[0];
-    assertThat(standardComparisonStrategy.areEqual(a1, a2)).isFalse();
+  void should_return_false_if_float_arrays_are_not_equal() {
+    // GIVEN
+    float[] actual = { 1.0f, 2.0f, 3.0f };
+    float[] other = { 4.0f, 5.0f, 6.0f };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
   }
 
   @Test
-  void should_return_false_if_arrays_of_primitives_are_not_equal() {
-    int[] a1 = { 6, 8, 10 };
-    boolean[] a2 = { true };
-    assertThat(standardComparisonStrategy.areEqual(a1, a2)).isFalse();
+  void should_return_true_if_double_arrays_are_equal() {
+    // GIVEN
+    double[] actual = { 1.0, 2.0, 3.0 };
+    double[] other = { 1.0, 2.0, 3.0 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
+  }
+
+  @Test
+  void should_return_false_if_double_arrays_are_not_equal() {
+    // GIVEN
+    double[] actual = { 1.0, 2.0, 3.0 };
+    double[] other = { 4.0, 5.0, 6.0 };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
+  }
+
+  @Test
+  void should_return_true_if_boolean_arrays_are_equal() {
+    // GIVEN
+    boolean[] actual = { true, false };
+    boolean[] other = { true, false };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isTrue();
+  }
+
+  @Test
+  void should_return_false_if_boolean_arrays_are_not_equal() {
+    // GIVEN
+    boolean[] actual = { true, false };
+    boolean[] other = { false, true };
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isFalse();
+  }
+
+  @Test
+  void should_fail_if_equals_implementation_fails() {
+    // GIVEN
+    Object actual = new EqualsThrowsException();
+    // WHEN
+    Throwable thrown = catchThrowable(() -> underTest.areEqual(actual, new Object()));
+    // THEN
+    then(thrown).isInstanceOf(RuntimeException.class)
+                .hasMessage("Boom!");
+  }
+
+  private static class EqualsThrowsException {
+
+    @Override
+    public boolean equals(Object obj) {
+      throw new RuntimeException("Boom!");
+    }
+
+  }
+
+  @ParameterizedTest
+  @MethodSource({ "correctEquals", "contractViolatingEquals" })
+  void should_delegate_to_equals_implementation_if_actual_is_not_null(Object actual, Object other, boolean expected) {
+    // WHEN
+    boolean result = underTest.areEqual(actual, other);
+    // THEN
+    then(result).isEqualTo(expected);
+  }
+
+  private static Stream<Arguments> correctEquals() {
+    Object object = new Object();
+
+    return Stream.of(arguments(object, null, false),
+                     arguments(object, object, true),
+                     arguments(object, new Object(), false),
+                     arguments("Luke", null, false),
+                     arguments("Luke", "Luke", true),
+                     arguments("Luke", "Yoda", false));
+  }
+
+  private static Stream<Arguments> contractViolatingEquals() {
+    AlwaysTrue alwaysTrue = new AlwaysTrue();
+    AlwaysFalse alwaysFalse = new AlwaysFalse();
+
+    NonReflexive nonReflexiveX = new NonReflexive();
+
+    NonSymmetric nonSymmetricY = new NonSymmetric(null);
+    NonSymmetric nonSymmetricX = new NonSymmetric(nonSymmetricY);
+
+    NonTransitive nonTransitiveZ = new NonTransitive(null, null);
+    NonTransitive nonTransitiveY = new NonTransitive(nonTransitiveZ, null);
+    NonTransitive nonTransitiveX = new NonTransitive(nonTransitiveY, nonTransitiveZ);
+
+    NonConsistent nonConsistentX = new NonConsistent();
+
+    return Stream.of(arguments(alwaysTrue, null, true),
+                     arguments(alwaysFalse, alwaysFalse, false),
+                     arguments(nonReflexiveX, nonReflexiveX, false),
+                     arguments(nonSymmetricX, nonSymmetricY, true),
+                     arguments(nonSymmetricY, nonSymmetricX, false),
+                     arguments(nonTransitiveX, nonTransitiveY, true),
+                     arguments(nonTransitiveY, nonTransitiveZ, true),
+                     arguments(nonTransitiveX, nonTransitiveZ, false),
+                     arguments(nonConsistentX, nonConsistentX, true),
+                     arguments(nonConsistentX, nonConsistentX, false),
+                     arguments(nonConsistentX, nonConsistentX, true));
+  }
+
+  private static class AlwaysTrue {
+
+    @Override
+    public boolean equals(Object obj) {
+      return true;
+    }
+
+    @Override
+    public String toString() {
+      return "always true";
+    }
+
+  }
+
+  private static class AlwaysFalse {
+
+    @Override
+    public boolean equals(Object obj) {
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return "always false";
+    }
+
+  }
+
+  private static class NonReflexive {
+
+    @Override
+    public boolean equals(Object obj) {
+      return this != obj;
+    }
+
+    @Override
+    public String toString() {
+      return "non reflexive";
+    }
+
+  }
+
+  private static class NonSymmetric {
+
+    private final Object other;
+
+    NonSymmetric(Object other) {
+      this.other = other;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return obj == other;
+    }
+
+    @Override
+    public String toString() {
+      return "non symmetric";
+    }
+
+  }
+
+  private static class NonTransitive {
+
+    private final Object y, z;
+
+    NonTransitive(Object y, Object z) {
+      this.y = y;
+      this.z = z;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return obj == y || obj != z;
+    }
+
+    @Override
+    public String toString() {
+      return "non transitive";
+    }
+
+  }
+
+  private static class NonConsistent {
+
+    private int i = 0;
+
+    @Override
+    public boolean equals(Object obj) {
+      return (++i % 2) != 0;
+    }
+
+    @Override
+    public String toString() {
+      return "non consistent";
+    }
+
   }
 
 }
diff --git a/src/test/java/org/assertj/core/internal/objects/Objects_assertEqual_Test.java b/src/test/java/org/assertj/core/internal/objects/Objects_assertEqual_Test.java
--- a/src/test/java/org/assertj/core/internal/objects/Objects_assertEqual_Test.java
+++ b/src/test/java/org/assertj/core/internal/objects/Objects_assertEqual_Test.java
@@ -83,7 +83,7 @@ void should_fail_if_objects_are_not_equal_according_to_custom_comparison_strateg
   void should_fail_if_compared_with_null() {
     Throwable error = catchThrowable(() -> objects.assertEqual(someInfo(), new MyObject(), null));
 
-    assertThat(error).isInstanceOf(AssertionError.class);
+    assertThat(error).isInstanceOf(MyObject.NullEqualsException.class);
   }
 
   @Test
@@ -94,6 +94,7 @@ void should_fail_with_my_exception_if_compared_with_other_object() {
   }
 
   private static class MyObject {
+
     private final int anInt = 0;
 
     @Override
@@ -105,12 +106,14 @@ public boolean equals(Object o) {
       return anInt == myObject.anInt;
     }
 
-    private class NullEqualsException extends RuntimeException {
+    private static class NullEqualsException extends RuntimeException {
       private static final long serialVersionUID = 6906581676690444515L;
     }
 
-    private class DifferentClassesException extends RuntimeException {
+    private static class DifferentClassesException extends RuntimeException {
       private static final long serialVersionUID = -7330747471795712311L;
     }
+
   }
+
 }