That's a pretty curious case. Let's understand the cause of such behavior first and then we'll try to figure out a solution.
When we dive into Mockito code, we'll find out that in this case the exception is thrown here (CallsRealMethods
class):
public void validateFor(InvocationOnMock invocation) {
if (new InvocationInfo(invocation).isAbstract()) {
throw cannotCallAbstractRealMethod();
}
}
Verification if the method is abstract is based on this code (InvocationInfo
class):
public boolean isAbstract() {
return (method.getModifiers() & Modifier.ABSTRACT) != 0;
}
which simply verifies if the method has an abstract flag (bit) set and in case of the CrudRepository
interface (where deleteAll
and findById
are sourced) it is obviously true.
As the method calls from Spring-managed repositories are proxied (in this case - to the SimpleJpaRepository
instance, read more here), Mockito has no way of knowing that the real method called here would not cause problems - it is abstract, but it is intercepted by Spring. So basically - Mockito is right to throw such an exception in this case without any additional context.
What can we do about it? There may be various ways of working around that after you know the cause of the problem - I will show you the first one that came to my mind, but I suppose there may be other (and better). I simply covered the abstract method from the interface with a fake class that delegates the tested method calls to the real repository. Such fake class instance can be easily spied on using Mockito - thanks to that the code you've provided works just fine (I've missed the fact that you were stubbing deleteAll
method, so in my case it's findById
). Please, see the code below as well as the inline comments.
// should not be treated as a Spring bean
// and should not be loaded into the Spring Context
@ConditionalOnExpression("false")
class MyRepositoryFake implements MyRepository {
private MyRepository delegate;
MyRepositoryFake(MyRepository delegate) {
this.delegate = delegate;
}
@Override
public Optional<MyEntity> findById(Long id) {
// all methods spied on in the tests should be delegated like this
// if you're using Lombok, consider:
// https://projectlombok.org/features/experimental/Delegate
return delegate.findById(id);
}
// rest of the methods...
}
@Autowired
MyRepository repository;
@Test
void test() {
// we're wrapping the repository to avoid
// Mockito interpreting the mocked method as abstract
// and then we're spying on it to make it easy to modify its behaviour
var repositorySpy = spy(new MyRepositoryFake(repository));
var id = 1L;
doThrow(new RuntimeException("first"))
.doCallRealMethod()
.doThrow(new RuntimeException("second"))
.when(repositorySpy)
.findById(id);
assertThatThrownBy(() -> repositorySpy.findById(id))
.hasMessage("first");
assertThat(repositorySpy.findById(id))
.isEmpty();
assertThatThrownBy(() -> repositorySpy.findById(id))
.hasMessage("second");
}
I've reproduced it in a GitHub repository, where you can find all the code. The test shown above (and in the repo) passes.