Search code examples
springspring-bootkotlinjunit5spring-cache

Test Spring Boot cache in Kotlin and JUnit5


I have a simple repository and it's interface written in Kotlin to get a list of sites from db; and I cache response with Spring cache:

interface IRepository {
  fun sites(): List<String>
}

@Repository
class Repository(private val jdbcTemplate: NamedParameterJdbcTemplate) : IRepository {
  private val sites = "SELECT DISTINCT siteId FROM sites"

  @Cacheable(value = ["sites"], key = "sites")
  override fun sites(): List<String> = jdbcTemplate.jdbcTemplate.queryForList(sites, String::class.java)
}

Now I want to test that caching is actually working. As base for test I used How to test Spring's declarative caching support on Spring Data repositories? but direct implementation resulted in error of repository being a proxy and not repository. So my current attempt is:

@ContextConfiguration
@ExtendWith(SpringExtension::class)
class RepositoryCacheTests {
  @MockBean
  private lateinit var repository: Repository

  @Autowired
  private lateinit var cache: CacheManager

  @EnableCaching
  @TestConfiguration
  class CachingTestConfig {
    @Bean
    fun cacheManager(): CacheManager = ConcurrentMapCacheManager("sites")
  }

  @Test
  fun `Sites is cached after first read`() {
    // Arrange
    whenever(repository.sites()).thenReturn(listOf(site, anotherSite))

    repository.sites()

    // Assert
    assertThat(cache.getCache("sites")?.get("sites")).isNotNull
  }

But cache is empty and not populated after first read. What am I missing in my setup?

Update:

Using George's suggestion I updated the test (and code for easier mocking). I also had to add @Bean for repository in configuration because of Could not autowire. No beans of 'Repository' type found. without it.

  @Cacheable(value = ["sites"], key = "'sites'")
  override fun sites(): List<String> = jdbcTemplate.query(sites) { rs, _ -> rs.getString("siteId") }
@ContextConfiguration
@ExtendWith(SpringExtension::class)
class RepositoryCacheTests {
  @MockBean
  private lateinit var jdbcTemplate: NamedParameterJdbcTemplate

  @Autowired
  private lateinit var repository: Repository

  @Autowired
  private lateinit var cache: CacheManager

  @EnableCaching
  @TestConfiguration
  class CachingTestConfig {
    @Bean
    fun testRepository(jdbcTemplate: NamedParameterJdbcTemplate): Repository = Repository(jdbcTemplate)

    @Bean
    fun cacheManager(): CacheManager = ConcurrentMapCacheManager("sites")
  }

  @Test
  fun `Sites is cached after first read`() {
    whenever(jdbcTemplate.query(any(), any<RowMapper<String>>())).thenReturn(listOf(site, anotherSite))
    repository.sites()
    assertThat(cache.getCache("sites")?.get("sites")).isNotNull
    repository.sites()
    verify(jdbcTemplate, times(1)).query(any(), any<RowMapper<String>>())
  }
}

Now the test does not even start:

Error creating bean with name 'RepositoryCacheTests': Unsatisfied dependency expressed through field 'repository'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'testRepository' is expected to be of type 'Repository' but was actually of type 'com.sun.proxy.$Proxy52'

Update 2:

As George points, solution is (https://stackoverflow.com/a/44911329/492882 and https://stackoverflow.com/a/44911329/492882)

  @Autowired
  private lateinit var repository: IRepository

Solution

  • You are mocking your test subject Repository repository. This should be the real object initialized by Spring so it has caching. You need to mock the JdbcTemplate that your test subject is calling.

    I don't really know kotlin syntax, so bear with me. Here's how your test should look like:

    @ContextConfiguration
    @ExtendWith(SpringExtension::class)
    class RepositoryCacheTests {
      @MockBean
      private lateinit jdbcTemplate: NamedParameterJdbcTemplate
      @Autowired
      private lateinit var repository: IRepository
    
      @Autowired
      private lateinit var cache: CacheManager
    
      @EnableCaching
      @TestConfiguration
      class CachingTestConfig {
        @Bean
        fun cacheManager(): CacheManager = ConcurrentMapCacheManager("sites")
      }
    
      @Test
      fun `Sites is cached after first read`() {
        // Arrange
        whenever(jdbcTemplate.queryForList(any(), String::class.java)).thenReturn(listOf(site, anotherSite))
    
        repository.sites()
    
        // Assert
        assertThat(cache.getCache("sites")?.get("sites")).isNotNull
    
        //Execute again to test cache.
        repository.sites()
        //JdbcTemplate should have been called once.
        verify(jdbcTemplate, times(1)).queryForList(any(), String::class.java)
      }