Search code examples
spring-bootcachingjunit5

Testing SpringBoot @Cacheable functionality


I've implemented Cache for some DDBB access on a Spring Boot application. So far it's been working, but we want to ensure it's not disabled in the furture, so I want to add a JUnit test to assert it on every compile round.

I can't make it work. I've followed instructions from other SO threads with no success Test Spring Boot cache in Kotlin and JUnit5

Our relevant stack for this matter is SpringBoot 2.7, JUnit 5

The @Cacheable enabled class is this

@Component
public class AttributeDatabaseAdapter implements AttributePort {
    
    final AttributeRepository repository;
    final AttributeMapper mapper;
    
    
    
    public AttributeDatabaseAdapter(AttributeRepository repository, AttributeMapper mapper) {
        this.repository = repository;
        this.mapper = mapper;
    }
    

    @Override
    @Cacheable(value = CacheConfig.CACHE_ATTRIBUTE, key="{ #attributeName, #userId }")
    public Attribute getActiveAttributeByName(String attributeName, Integer userId) {
        List<AttributeEntity> listAttribute = repository.findAll(AttributeSpecifications.getFilterFromSelector(attributeName));
        
        if (!listAttribute.isEmpty()) {
            if (listAttribute.size() == 1) {
                return mapper.toDomainObject(listAttribute.get(0));
            } else {
                throw new NonUniqueResultException("A unique result was expected but more elements were found.");
            }
        }
        return new Attribute();
    }

}

Now for testing, I've build Up this JUnit file, following this Baeldung tutorial and spiced-up with this StackOverflow post

@ContextConfiguration
@ExtendWith(SpringExtension.class)
public class AttributeDatabaseAdapterTest {

    @Mock
    private static AttributeRepository repositoryMock;

    @Spy
    private static AttributeMapper attributeMapper = new AttributeMapperImpl();
    
    @EnableCaching
    @Configuration
    @Import({AttributeMapperImpl.class})
    public static class CachingTestConfig {
        @Bean
        public CacheManager cacheManager() {
            return new ConcurrentMapCacheManager(CacheConfig.CACHE_ATTRIBUTE);
        }

        @Bean
        public AttributeRepository repository() {
            return repositoryMock;
        }
    }
    
    // OPTION 1
    // @InjectMocks private AttributeDatabaseAdapter databaseAdapter;
    
    // OPTION 2
    @Autowired private AttributePort databaseAdapter;

    @BeforeEach
    void setUp() {
        reset(repositoryMock);
    }
    
    private final static AttributeEntity ATTR1 = AttributeEntity.builder().techId(1).build();
    private final static AttributeEntity ATTR2 = AttributeEntity.builder().techId(2).build();

    @Test
    @DisplayName("Calling DatabaseAdapter two times makes one call to repository")
    void multiple_calls_must_hit_databaseadapter_only_once() {
        when(repositoryMock.findAll(any(Specification.class))).thenReturn(Arrays.asList(ATTR1));
        
        // Mapper makes new instances, so can't compare objects. Must compare values
        assertEquals(ATTR1.getTechId(), databaseAdapter.getActiveAttributeByName("TEST", 1).getTechId());
        assertEquals(ATTR1.getTechId(), databaseAdapter.getActiveAttributeByName("TEST", 1).getTechId());
        assertEquals(ATTR1.getTechId(), databaseAdapter.getActiveAttributeByName("TEST", 1).getTechId());
        
        verify(repositoryMock, times(1)).findAll(any(Specification.class)); // Fails on OPTION 1
    }
    
    @Test
    @DisplayName("Assert Repository must return one single result")
    void filtering_Must_Return_single_Result() {
        when(repositoryMock.findAll(any(Specification.class))).thenReturn(Arrays.asList(ATTR1, ATTR2));
        assertThrows(NonUniqueResultException.class, () -> databaseAdapter.getActiveAttributeByName("TEST", 1));
    }
}

Both OPTION 1 and OPTION 2 fails:

  1. OPTION-1 complains on test 1, as assert finds 3 hits on DatabaseAdatper, expected 1. Seems SpringBoot @Cacheable is not working. Maybe SpringBoot Context is not running. Test 2 runs perfectly
  2. OPTION-2 doesn't start. Complains on missing AttributePort bean. Seems Autowiring cannot find implementation properly

So, what I am missing?

According to this post OPTION-2 must declare a varaible using the interface instead of the implementation for SpringBoot to autowire properly. but it's not working. OPTION-1 seems @Cacheable not being initialized


Solution

  • Option 1 won't work because if you use @InjectMocks, you're letting Mockito create an instance of your AttributeDatabaseAdapter, which won't be a Spring bean (and thus @Cacheable will be ignored).

    Option 2 is the right solution. However, right now it isn't working because nowhere in your test you tell the Spring container to include AttributeDatabaseAdapter. The easiest way to do this is to extend the @Import annotation on your configuration. For example:

    @EnableCaching
    @Configuration
    @Import({AttributeMapperImpl.class, AttributeDatabaseAdapter.class}) // Change this
    public static class CachingTestConfig {
        // ...
    
    }
    

    There are also a few other caveats in your code.

    1. You're combining Mockito-annotations with Spring-configuration which won't work because you're not using the MockitoExtension (nor should you if you want to test @Cacheable). This means that:

      • You shouldn't use @Mock but use @MockBean in stead (you can remove the repository() method from your CachingTestConfig).
      • You shouldn't use @Spy. Right now it doesn't even do much because you're already importing the AttributeMapperImpl bean through the @Import annotation so you could just remove the spy alltogether.
    2. Ideally you use @TestConfiguration in stead of @Configuration in your CachingTestConfig.