16

I am struggling with testing @Cacheable within a Spring Boot Integration Test. This is my second day learning how to do Integration Tests and all of the examples I have found use older versions. I also saw an example of assetEquals("some value", is()) but nothing with an import statement to know which dependency "is" belongs to. The test fails at the second

This is my integration test....

@RunWith(SpringRunner.class)
@DataJpaTest // used for other methods
@SpringBootTest(classes = TestApplication.class)
@SqlGroup({
        @Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD,
                scripts = "classpath:data/Setting.sql") })
public class SettingRepositoryIT {

    @Mock
    private SettingRepository settingRepository;

    @Autowired
    private Cache applicationCache;


    @Test
    public void testCachedMethodInvocation() {
        List<Setting> firstList = new ArrayList<>();
        Setting settingOne = new Setting();
        settingOne.setKey("first");
        settingOne.setValue("method invocation");
        firstList.add(settingOne);

        List<Setting> secondList = new ArrayList<>();
        Setting settingTwo = new Setting();
        settingTwo.setKey("second");
        settingTwo.setValue("method invocation");
        secondList.add(settingTwo);

        // Set up the mock to return *different* objects for the first and second call
        Mockito.when(settingRepository.findAllFeaturedFragrances()).thenReturn(firstList, secondList);

        // First invocation returns object returned by the method
        List<Setting> result = settingRepository.findAllFeaturedFragrances();
        assertEquals("first", result.get(0).getKey());

        // Second invocation should return cached value, *not* second (as set up above)
        List<Setting> resultTwo = settingRepository.findAllFeaturedFragrances();
        assertEquals("first", resultTwo.get(0).getKey()); // test fails here as the actual is "second."

        // Verify repository method was invoked once
        Mockito.verify(settingRepository, Mockito.times(1)).findAllFeaturedFragrances();
        assertNotNull(applicationCache.get("findAllFeaturedFragrances"));

        // Third invocation with different key is triggers the second invocation of the repo method
        List<Setting> resultThree = settingRepository.findAllFeaturedFragrances();
        assertEquals(resultThree.get(0).getKey(), "second");
    }
}

ApplicationContext, components, entities, repositories and service layer for tests. The reason why I do it this way is because this maven module is used in other modules as a dependency.

@ComponentScan({ "com.persistence_common.config", "com.persistence_common.services" })
@EntityScan(basePackages = { "com.persistence_common.entities" })
@EnableJpaRepositories(basePackages = { "com.persistence_common.repositories" })
@SpringBootApplication
public class TestApplication {

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

Cache config....

@Configuration
@EnableCaching
public class CacheConfig {

    public static final String APPLICATION_CACHE = "applicationCache";

    @Bean
    public FilterRegistrationBean registerOpenSessionInViewFilterBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        OpenEntityManagerInViewFilter filter = new OpenEntityManagerInViewFilter();
        registrationBean.setFilter(filter);
        registrationBean.setOrder(5);
        return registrationBean;
    }


    @Bean
    public Cache applicationCache() {
        return new GuavaCache(APPLICATION_CACHE, CacheBuilder.newBuilder()
                .expireAfterWrite(30, TimeUnit.DAYS)
                .build());
    }
}

The repository under test....

public interface SettingRepository extends JpaRepository<Setting, Integer> {

    @Query(nativeQuery = true, value = "SELECT * FROM Setting WHERE name = 'featured_fragrance'")
    @Cacheable(value = CacheConfig.APPLICATION_CACHE, key = "#root.methodName")
    List<Setting> findAllFeaturedFragrances();
}
Roman Vottner
  • 12,213
  • 5
  • 46
  • 63
Grim
  • 2,398
  • 4
  • 35
  • 54
  • 1
    `is(...)` will most probably relate to [Hamcrest CoreMatcher's `is`](https://github.com/hamcrest/JavaHamcrest/blob/master/hamcrest-core/src/main/java/org/hamcrest/CoreMatchers.java#L104). Hamcrest is often used in combination with JUnits `assertThat(actual, matcher)` as it provides a more fluent reading-style of the assertions. I also [asked a question regarding Spring cache](http://stackoverflow.com/questions/29562642/caching-of-nested-cacheable-operation-via-springcache) some time ago where I used a unit test to depict my issue. Maybe it is helpful to you in some way – Roman Vottner May 17 '17 at 15:21
  • ok thank you Roman... – Grim May 18 '17 at 23:13

2 Answers2

9

The first problem with SettingRepositoryIT is, the @Mock anotation on the field settingRepository. This is paradox for any normal-test, integration-test or any else.

You should let Spring bring in the dependencies for the class-under-test, which is SettingRepository in your case.

Please look at this example how @Autowired is used for the class-under-test, which is OrderService in this example:

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from the
// static nested Config class
@ContextConfiguration
public class OrderServiceTest {

    @Configuration
    static class Config {

        // this bean will be injected into the OrderServiceTest class
        @Bean
        public OrderService orderService() {
            OrderService orderService = new OrderServiceImpl();
            // set properties, etc.
            return orderService;
        }
    }

    @Autowired
    private OrderService orderService;

    @Test
    public void testOrderService() {
        // test the orderService
    }

}

Go for the documentation with the full example: § 15. Integration Testing

The second problem is that you do not have to test @Cachable. You should only test your implementation. Here is a very good example from Oliver Gierke on how you should test it: How to test Spring's declarative caching support on Spring Data repositories?

Tobias Otto
  • 1,634
  • 13
  • 20
  • 11
    It's not correct that "you do not have to test Cachable", to feel safe I should have tools to test Cachable. If not, how can I be sure that all my JPA cache settings are correct? All needed annotations are in place? All the cache beans are in the container at run-time? I cannot trust it without testing. Anyways I will test Cachable with Spring Test or without it by making test example. I feel like not having the possibility to test it with SpringTest is a disadvantage. – Andreas Gelever Jul 07 '18 at 13:29
  • What about [Cannot test Spring Caching in Unit Test](https://stackoverflow.com/questions/69313791/cannot-test-spring-caching-in-unit-test)? – Jack Sep 24 '21 at 11:04
1

In my case I wanted to validate the expression in the unless expression in the @Cacheable annotation, so I think it makes perfect sense and I'm not testing Spring's code.

I managed to test it without using Spring Boot, so it is plain Spring test:

@RunWith(SpringRunner.class)
@ContextConfiguration
public class MyTest {

    private static MyCacheableInterface myCacheableInterfaceMock = mock(MyCacheableInterface.class);

    @Configuration
    @EnableCaching
    static class Config {

        @Bean
        public MyCacheableInterface myCacheableInterface() {
            return myCacheableInterfaceMock;
        }

        @Bean
        public CacheManager cacheManager() {
            return new ConcurrentMapCacheManager("myObject");
        }
    }

    @Autowired
    private MyCacheableInterface myCacheableInterface;

    @Test
    public void test() {
        when(myCacheableInterfaceMock.businessMethod(anyString())).then(i -> {
            List<MyObject> list = new ArrayList<>();
            list.add(new MyObject(new Result("Y")));
            return list;
        });
        myCacheableInterface.businessMethod("test");
        verify(myCacheableInterfaceMock).businessMethod(anyString());
        myCacheableInterface.businessMethod("test");
        verifyNoMoreInteractions(myCacheableInterfaceMock);
    }
}

In MyCacheableInterface I have the following annotation:

public interface MyCacheableInterface {
    @Cacheable(value = "myObject", unless = "#result.?[Result.getSuccess() != 'Y'].size() == #result.size()")
    List<MyObject> businessMethod(String authorization);
}