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:
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
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.
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:
@Mock
but use @MockBean
in stead (you can remove the repository()
method from your CachingTestConfig
).@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.Ideally you use @TestConfiguration
in stead of @Configuration
in your CachingTestConfig
.