spring-boot: @SpyBean does not work when used to spy on a Spring Data Repository

Version: Spring Boot 1.4.1 Subject: @SpyBean on Data Jpa Repository bean isn’t working Exception thrown:

UnsatisfiedDependencyException: Error creating bean with name 'cityService' defined in file [...\target\classes\com\example\CityService.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cityRepository': Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: Object of class [org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean] must be an instance of interface com.example.CityRepository

Demonstration project: https://github.com/igormukhin/spring-boot-issue-6871 USE BRANCH: spybean-on-jparepository

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 53
  • Comments: 66 (18 by maintainers)

Commits related to this issue

Most upvoted comments

@philwebb I stunble upon it as I worked on an integration test where I wanted to check if a specific save operation is called at the end. But still I needed that all other (find… methods) to work as usual.

As a workaround I’m checking if the saved entry is in the database.

At the end of the day, it’s not too often people have to use spies but there are cases when it’s the option that gets the job done.

+1. Same issue for the same reason (wanting to verify interactions). Using spring-data-mongo, though.

I followed @hashpyrit’s advice, using a special configuration for tests. It works well and is completely decoupled from the test cases.

@Configuration
public class MockRepositoryConfiguration {

    @Primary
    @Bean(name = "fooRepositoryMock")
    FooRepository fooRepository(final FooRepository real) {
        // workaround for https://github.com/spring-projects/spring-boot/issues/7033
        return Mockito.mock(FooRepository.class, AdditionalAnswers.delegatesTo(real));
    }
}

We have a fix for this. It’s a little bit risky, but not too bad. As such, we’ve decided that 2.5.x is the best place for it.

I encountered this problem as well. I got around it by creating my mock as follows:

MyClass spyOfSpringProxy = Mockito.mock(MyClass.class, AdditionalAnswers.delegatesTo(springCreatedProxy));

AdditionalAnswers.deletegateTo() works because the object you are delegating to does NOT have to be the same type as the type of the mock. I was able to use it in the same manner as a regular Mockito spy and was able to verify interactions.

The workaround still has a bug, in that it does not reset the mock between tests, like spies are.

Instead you need to set it to reset as follows:

    @Configuration
    public static class MockConfiguration {
        @Autowired
        private MeetingsRepository meetingsRepositoryReal;

        // We need a delegating mock because spy does not work on JPA repositories.
        @Primary
        @Bean
        public MeetingsRepository delegatingMeetingsRepository() {
            return mock(MeetingsRepository.class, MockReset.withSettings(MockReset.AFTER)
                            .defaultAnswer(AdditionalAnswers.delegatesTo(meetingsRepositoryReal)));
        }
    }

+1. Same reason here

Hi there. Another use case could be to use @SpyBean on a repository to test that a service with @Cacheable annotation will call the repository once and the second time the value is returned from the cache.

Thanks, @eiswind. To help others facing your problem, I wanted to make a note of what we’ve learned while looking at #21488.

In short, anyone using the workaround above from @kuhnroyal with Spring Boot 2.3 and the default deferred bootstrapping behaviour may need to use an ObjectProvider when injecting the repository into their test class:

@Autowired
ObjectProvider<MyRepository> repository;

We’ll try to take another look at this one and see if we can remove the need for the workaround by getting @SpyBean to work in this scenario.

I’m posting another variant of @AstralStorm’s workaround.

When the original bean is @Validate Mockito has trouble with Hibernate. And I used a way from this.

            return Mockito.mock(MyBeanClass.class,
                                MockReset.withSettings(MockReset.AFTER)
                                        .withoutAnnotations()
                                        .defaultAnswer(AdditionalAnswers.delegatesTo(myBean)));

Even 3 years later it’s still a problem 😃?

If you look at what Mockito.spy(…) does, it’s essentially a Mockito.mock(…) but with a default answer of calling the real method. So it’s sort of the same I was achieving with my BeanPostProcessor. It looks like you can avoid the real methods being called by using the doReturn(…).when(spy).methodOnSpy() style of stubbing as documented in Mockito.spy(…).

Let’s see what the others say how to proceed.

When wanting to test interactions isn’t a plain unit test of the client with a mock of the repository sufficient? It wouldn’t even need to be an integration then would it?

It feels like you’re mixing up two aspects of testing here: integration tests that check the behavior end to end and the desire to verify on internals of the tested component. I’d argue that’s sort of violating the SRP principle in tests (if there is such thing in tests). Either test the thing as whole, then it’s inputs against output. Or a dedicated component whose interaction with collaborators you inspect in detail.

This issue has been chasing milestones for 5 years now…

I have seen that some specific jdk+mockito+spring version combinations appear to somehow work while other throw the UnsatisfiedDependencyExceptions.

If the issue is not going to be fixed anytime soon, could we perhaps publish a list of jdk+mockito+spring version combinations that are known to work?

I can confirm that:

openjdk version “11.0.11-ea” 2021-04-20 + mockito-core 3.6.0 + spring-test 5.3.1 from spring boot 2.4.0 does need a workaround mentioned by @onacit

I have a strange regression here. After upgrading to 2.3.0.RELEASE none of the above workarounds seem to work.

It can only guess that something has changed in the test context creation.

Let me show you my setup:

@SpringBootTest(webEnvironment =
        SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("inmemory")
class RegistrationIntegrationTest {

    @TestConfiguration
    static class MockRepositoryConfiguration {

        @Primary
        @Bean
        NewsUserRepository newsUserRepositoryMock(NewsUserRepository real) {
            return Mockito.mock(NewsUserRepository.class,
                    AdditionalAnswers.delegatesTo(real));
        }
    }

    @Autowired
    NewsUserRepository newsUserRepository;

I also tried a setup with a BeanPostProcessor like in https://gist.github.com/phillipuniverse/4b3d39cdcceb2363a14ebdcc170d9059

In both scenarios the injected NewsUserRepository is not the mocked one, but tha vanilla SimpleJpaRepository. I can see that the mock gets created, but setting a breakpoint in BeforeEach shows something different.

Bildschirmfoto von 2020-05-18 11-42-53

Am I getting something wrong? If not, this likely seems to be a different problem with the test creation.

Any update on progress made? Ideally I would just like to add @SpyBean in front of my repository in the test and just have it work (rather than dealing with workarounds).

My specific use-case was verifying that database transactions were being rolled back as expected if a call to the repository (made in a service method) failed. Transaction was being applied as the service level. Hence I wanted to make the repository throw an exception. I wanted to use the functionality real repository most of the time except for the failure case. A spy was appropriate here. The component under test was the service but due to the nature of what it was doing, simply mocking the repository would not have been as complete a test as using a real repository.