Wanting to test some lock enforcement in my services I tried to "decorate" existing jpa repository methods to add gate keeping for order of operations using spies.

To my surprise repository interfaces and @SpyBean don't work when dealing with callRealMethod() configurations but normal interface based beans work just fine.

plugins {
    java
    id("org.springframework.boot") version "3.1.2"
    id("io.spring.dependency-management") version "1.1.2"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test> {
    useJUnitPlatform()
}
package com.example.test;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;

@SpringBootApplication
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}

interface DummyService {
    String some_call();
}

@Service
class DummyServiceImpl implements DummyService {
    @Override
    public String some_call() {
        return "works";
    }
}


@Entity
class Person {
    @Id
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

interface PersonRepository extends JpaRepository<Person, String> {}
package com.example.test;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class TestApplicationTests {
    @SpyBean
    PersonRepository personRepository;

    @SpyBean
    DummyService dummyService;


    @Test
    void failing_repos_spy() {
        Mockito
            .doAnswer(invocation -> {
                // some other logic
                return invocation.callRealMethod();
            })
            .when(personRepository)
            .findAll();

        assertThat(personRepository.findAll()).isEmpty();
    }

    @Test
    void working_service_spy() {
        Mockito
            .doAnswer(invocation -> {
                // some other logic
                return invocation.callRealMethod();
            })
            .when(dummyService)
            .some_call();

        assertThat(dummyService.some_call()).isEqualTo("works");
    }
}
Cannot call abstract real method on java object!
Calling real methods is only possible when mocking non abstract method.
  //correct example:
  when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
org.mockito.exceptions.base.MockitoException: 
Cannot call abstract real method on java object!
Calling real methods is only possible when mocking non abstract method.
  //correct example:
  when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
    at app//com.example.test.TestApplicationTests.lambda$failing_repos_spy$0(TestApplicationTests.java:23)
    at app//com.example.test.TestApplicationTests.failing_repos_spy(TestApplicationTests.java:28)

Comment From: wilkinsona

The behavior you are using is a consequence of how Spring Data repositories and Mockito spies are implemented and the fix for https://github.com/spring-projects/spring-boot/issues/7033.

To fix #7033, the spy is created using the repository interface with a default answer that delegates calls to the Spring Data-generated implementation:

https://github.com/spring-projects/spring-boot/blob/922f66a85d3cdd151c137a6a84ec99b04566b618/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyDefinition.java#L107-L110

Spying on the repository interface (whose methods are all abstract) means that you cannot use callRealMethod(). Instead, you need to continue to delegate the call to the Spring Data-generated implementation. You can do that by making your Answer delegate to the default Answer:

Answer<?> defaultAnswer = Mockito
                              .mockingDetails(personRepository)
                              .getMockCreationSettings()
                              .getDefaultAnswer();
Mockito
    .doAnswer(invocation -> {
        // some other logic
        return defaultAnswer.answer(invocation);
    })
    .when(personRepository)
    .findAll();
assertThat(personRepository.findAll()).isEmpty();

Comment From: krodyrobi

Thank you for the detailed, yet concise summary!