Repository: mockito/mockito
Issue: Spy doesn't forward hashcode/equals to actual object

### Mockito 3.11.1

```
public class MockitoTest {

    private static class ValueHolder {
        private final String value;

        public ValueHolder( String value ) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        @Override public boolean equals( Object o ) {
            if (this == o) return true;
            if (!(o instanceof ValueHolder)) return false;
            ValueHolder that = (ValueHolder) o;
            return getValue().equals( that.getValue() );
        }

        @Override
        public int hashCode() {
            return Objects.hash( getValue() );
        }
    }

    @Test
    public void test() {
        ValueHolder value = new ValueHolder( "test" );
        ValueHolder spy = Mockito.spy( value );
        assert value.equals( spy );
        assert spy.equals( value ); //fails
        assert value.hashCode() == spy.hashCode(); //fails
    }
}
```

 - [x] The mockito message in the stacktrace have useful information, but it didn't help
 - [x] The problematic code (if that's possible) is copied here;
       Note that some configuration are impossible to mock via Mockito
 - [x] Provide versions (mockito / jdk / os / any other relevant information)
 - [x] Provide a [Short, Self Contained, Correct (Compilable), Example](http://sscce.org) of the issue
       (same as any question on stackoverflow.com)
 - [x] Read the [contributing guide](https://github.com/mockito/mockito/blob/main/.github/CONTRIBUTING.md)




Issue comments:
To create a mock a subclass of a mock is created through SubclassBytecodeGenerator.mockClass() here we can see that hashCode method is intercepted and handled by MockMethodInterceptor.ForHashCode. Here the code returns System.identityHashCode(thiz) as the hashcode where thiz is the object returned by Mockito.spy(). Same is the case of equals, where this==that is returned when equals is called.

@TimvdLippe shoud we add handling to delegate the call to hashCode and equals of spied object ?
Hm, I guess we could? I wonder if any tests start failing after that, but if they all pass I am okay with making that change yeah 👍 
I have opened a pull request [here](https://github.com/mockito/mockito/pull/2369).
@TimvdLippe could you please review it.
This issue needs to be reopened because it still reproduces with v3.12.4

Was fixed in v3.12.0 by 123beb8311ed6303ba71d682cff8434fe36b7f0f, but 1a8946f19cb8c677fe779f8acbda97e284f7d61f reverted the fix.
e1f98555ec904e8f8150cb24fa90392393cad9be also removed the test added by initial fix.
@raphw why was https://github.com/mockito/mockito/commit/1a8946f19cb8c677fe779f8acbda97e284f7d61f needed? (Note that the type cache was fixed 2h before your commit: https://github.com/mockito/mockito/commit/4f81d4f1de676f845d6ee111eb6285dbdd5935c6)
We use the same generated classes for both spy and mock classes. This is why we had to unify the behaviour. Internally, a spy is nothing but a mock with a prefefined delegatory answer. This is why a spy needs to behave like a mock.

Generally, I agree with that this behaviour would to be preferred. But it would require a lot of internal refactoring as we often rely on the hashcode/equals properties of mocks.
Stumbled upon this today.

If this cannot be fixed in the near future I would suggest to at least document this e.g. in de JavaDoc of `@Spy` annotation and Mockito.spy method.

Pull request review comments:
I am not sure if I follow why this line was changed. Could you elaborate as to why?
Consider the test [PartialMockingWithSpiesTest.shouldAllowStubbingOfMethodsThatDelegateToOtherMethods](https://github.com/mockito/mockito/blob/6f9108b833323345bd926f10914224d3e0a11b80/src/test/java/org/mockitousage/spies/PartialMockingWithSpiesTest.java#L81).
Now the call to getName is handled by [MockMethodInterceptor.DispatcherDefaultingToRealMethod.interceptSuperCallable](https://github.com/mockito/mockito/blob/6f9108b833323345bd926f10914224d3e0a11b80/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodInterceptor.java#L135) here the interceptor will not be null and it will go to interceptor.doIntercept. This finally leads to [InvocationMatcher.matches](https://github.com/mockito/mockito/blob/123beb8311ed6303ba71d682cff8434fe36b7f0f/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java#L79)

Now with changes in this pull request the call to equals is again delegated to  MockMethodInterceptor.DispatcherDefaultingToRealMethod and it will come at same place. And this way it leads to stackoverflow. Earlier equals of spy was this==that. So i just replced equals with ==.
Awesome, thanks for the explanation! That makes sense to me 😄 
Haha Thanks. Had missed to put links to the classes.

Patch that resolved the issue:
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java
index 553a02e248..7178927cd9 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java
@@ -393,7 +393,8 @@ public <T> Class<? extends T> createMockType(MockCreationSettings<T> settings) {
                             settings.getTypeToMock(),
                             settings.getExtraInterfaces(),
                             settings.getSerializableMode(),
-                            settings.isStripAnnotations()));
+                            settings.isStripAnnotations(),
+                            settings.getDefaultAnswer()));
         } catch (Exception bytecodeGenerationFailed) {
             throw prettifyFailure(settings, bytecodeGenerationFailed);
         }
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/MockFeatures.java b/src/main/java/org/mockito/internal/creation/bytebuddy/MockFeatures.java
index 1dbfba1334..ca27be01e1 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/MockFeatures.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/MockFeatures.java
@@ -8,6 +8,7 @@
 import java.util.Set;
 
 import org.mockito.mock.SerializableMode;
+import org.mockito.stubbing.Answer;
 
 class MockFeatures<T> {
 
@@ -15,23 +16,28 @@ class MockFeatures<T> {
     final Set<Class<?>> interfaces;
     final SerializableMode serializableMode;
     final boolean stripAnnotations;
+    final Answer defaultAnswer;
 
     private MockFeatures(
             Class<T> mockedType,
             Set<Class<?>> interfaces,
             SerializableMode serializableMode,
-            boolean stripAnnotations) {
+            boolean stripAnnotations,
+            Answer defaultAnswer) {
         this.mockedType = mockedType;
         this.interfaces = Collections.unmodifiableSet(interfaces);
         this.serializableMode = serializableMode;
         this.stripAnnotations = stripAnnotations;
+        this.defaultAnswer = defaultAnswer;
     }
 
     public static <T> MockFeatures<T> withMockFeatures(
             Class<T> mockedType,
             Set<Class<?>> interfaces,
             SerializableMode serializableMode,
-            boolean stripAnnotations) {
-        return new MockFeatures<T>(mockedType, interfaces, serializableMode, stripAnnotations);
+            boolean stripAnnotations,
+            Answer defaultAnswer) {
+        return new MockFeatures<T>(
+                mockedType, interfaces, serializableMode, stripAnnotations, defaultAnswer);
     }
 }
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java
index 8bf72feb89..dc1e81752c 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java
@@ -79,7 +79,8 @@ public <T> Class<? extends T> createMockType(MockCreationSettings<T> settings) {
                             settings.getTypeToMock(),
                             settings.getExtraInterfaces(),
                             settings.getSerializableMode(),
-                            settings.isStripAnnotations()));
+                            settings.isStripAnnotations(),
+                            settings.getDefaultAnswer()));
         } catch (Exception bytecodeGenerationFailed) {
             throw prettifyFailure(settings, bytecodeGenerationFailed);
         }
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java
index 3dadd3fa46..46ecb7868a 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java
@@ -44,6 +44,7 @@
 import net.bytebuddy.implementation.Implementation;
 import net.bytebuddy.implementation.attribute.MethodAttributeAppender;
 import net.bytebuddy.matcher.ElementMatcher;
+import org.mockito.Answers;
 import org.mockito.codegen.InjectionBase;
 import org.mockito.exceptions.base.MockitoException;
 import org.mockito.internal.creation.bytebuddy.ByteBuddyCrossClassLoaderSerializationSupport.CrossClassLoaderSerializableMock;
@@ -237,14 +238,24 @@ public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
                                 features.stripAnnotations
                                         ? MethodAttributeAppender.NoOp.INSTANCE
                                         : INCLUDING_RECEIVER)
-                        .method(isHashCode())
-                        .intercept(hashCode)
-                        .method(isEquals())
-                        .intercept(equals)
                         .serialVersionUid(42L)
                         .defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE)
                         .implement(MockAccess.class)
                         .intercept(FieldAccessor.ofBeanProperty());
+
+        if (features.defaultAnswer != Answers.CALLS_REAL_METHODS) {
+            builder =
+                    builder.method(isHashCode())
+                            .intercept(hashCode)
+                            .method(isEquals())
+                            .intercept(equals);
+        } else {
+            builder =
+                    builder.method(isHashCode())
+                            .intercept(dispatcher)
+                            .method(isEquals())
+                            .intercept(dispatcher);
+        }
         if (features.serializableMode == SerializableMode.ACROSS_CLASSLOADERS) {
             builder =
                     builder.implement(CrossClassLoaderSerializableMock.class)
diff --git a/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java b/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java
index ef475e60c6..6cc7a8c49f 100644
--- a/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java
+++ b/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java
@@ -77,7 +77,7 @@ public String toString() {
 
     @Override
     public boolean matches(Invocation candidate) {
-        return invocation.getMock().equals(candidate.getMock())
+        return invocation.getMock() == candidate.getMock()
                 && hasSameMethod(candidate)
                 && argumentsMatch(candidate);
     }


Test patch:
diff --git a/src/test/java/org/mockito/internal/creation/bytebuddy/TypeCachingMockBytecodeGeneratorTest.java b/src/test/java/org/mockito/internal/creation/bytebuddy/TypeCachingMockBytecodeGeneratorTest.java
index d0ed772a68..6ffe72950f 100644
--- a/src/test/java/org/mockito/internal/creation/bytebuddy/TypeCachingMockBytecodeGeneratorTest.java
+++ b/src/test/java/org/mockito/internal/creation/bytebuddy/TypeCachingMockBytecodeGeneratorTest.java
@@ -18,6 +18,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Answers;
 import org.mockito.mock.SerializableMode;
 import org.mockitoutil.VmArgAssumptions;
 
@@ -46,7 +47,8 @@ public void ensure_cache_is_cleared_if_no_reference_to_classloader_and_classes()
                                 classloader_with_life_shorter_than_cache.loadClass("foo.Bar"),
                                 Collections.<Class<?>>emptySet(),
                                 SerializableMode.NONE,
-                                false));
+                                false,
+                                Answers.RETURNS_DEFAULTS));
 
         ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
         Reference<Object> typeReference =
@@ -79,7 +81,8 @@ public void ensure_cache_returns_same_instance() throws Exception {
                                 classloader_with_life_shorter_than_cache.loadClass("foo.Bar"),
                                 Collections.<Class<?>>emptySet(),
                                 SerializableMode.NONE,
-                                false));
+                                false,
+                                Answers.RETURNS_DEFAULTS));
 
         Class<?> other_mock_type =
                 cachingMockBytecodeGenerator.mockClass(
@@ -87,7 +90,8 @@ public void ensure_cache_returns_same_instance() throws Exception {
                                 classloader_with_life_shorter_than_cache.loadClass("foo.Bar"),
                                 Collections.<Class<?>>emptySet(),
                                 SerializableMode.NONE,
-                                false));
+                                false,
+                                Answers.RETURNS_DEFAULTS));
 
         assertThat(other_mock_type).isSameAs(the_mock_type);
 
@@ -123,7 +127,8 @@ public void ensure_cache_returns_different_instance_serializableMode() throws Ex
                                 classloader_with_life_shorter_than_cache.loadClass("foo.Bar"),
                                 Collections.<Class<?>>emptySet(),
                                 SerializableMode.NONE,
-                                false));
+                                false,
+                                Answers.RETURNS_DEFAULTS));
 
         Class<?> other_mock_type =
                 cachingMockBytecodeGenerator.mockClass(
@@ -131,7 +136,8 @@ public void ensure_cache_returns_different_instance_serializableMode() throws Ex
                                 classloader_with_life_shorter_than_cache.loadClass("foo.Bar"),
                                 Collections.<Class<?>>emptySet(),
                                 SerializableMode.BASIC,
-                                false));
+                                false,
+                                Answers.RETURNS_DEFAULTS));
 
         assertThat(other_mock_type).isNotSameAs(the_mock_type);
     }
diff --git a/src/test/java/org/mockitousage/spies/SpyingOnRealObjectsTest.java b/src/test/java/org/mockitousage/spies/SpyingOnRealObjectsTest.java
index a3ed58cfd4..06b4878fa3 100644
--- a/src/test/java/org/mockitousage/spies/SpyingOnRealObjectsTest.java
+++ b/src/test/java/org/mockitousage/spies/SpyingOnRealObjectsTest.java
@@ -9,6 +9,7 @@
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.Mockito.*;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
@@ -192,4 +193,13 @@ public void shouldSayNiceMessageWhenSpyingOnPrivateClass() throws Exception {
                             "Most likely it is due to mocking a private class that is not visible to Mockito");
         }
     }
+
+    @Test
+    public void spysHashCodeEqualsDelegatedToActualMethods() {
+        List<String> real = new ArrayList<>();
+        real.add("one");
+        List<String> spy = spy(real);
+        assertEquals(real.hashCode(), spy.hashCode());
+        assertTrue(spy.equals(real));
+    }
 }