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!