1

I am writing a service test with JpaRepository. It works fine, but I want to check a case with a sequence of failures from a database.

I created a test and injected a @SpyBean as a bean of my Repository type. In general, I want to use a real repository bean in happy path scenarios, I just want to break it in a few cases when I want to simulate a failure.

It works fine, but I wanted to check a sequence of calls to a single method like this: exception -> ok -> exception - I work with batches, so that's why I would love to test it that way.

I tried to mock the behavior like this:

doThrow(...).doCallRealMethod().doThrow(...)
    .when(mySpyBean).deleteAll(any());

to simulate that sequence.

Unfortunately, it fails with an exception and a message like this:

Cannot call abstract real method on java object!
Calling real methods is only possible when mocking non abstract method.

Ok, I get that message, however I just want to call what would be called if I do not set up any methods Mockito methods (like default behavior). I just want to delegate a call to a real bean registered in Spring in the second call.

I also tried with doAnswer(...), but did not manage to find a correct solution for that.

Any ideas?

1 Answers1

1

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.

Jonasz
  • 1,617
  • 1
  • 13
  • 19