测试Repository上的@Cacheable

2023/05/11

1. 概述

除了实现之外,我们还可以使用Spring的声明式缓存机制将缓存注解用于接口。 例如,我们可以在Spring Data Repository上声明缓存注解。

2. 快速开始

首先,我们创建一个简单的模型:


@Entity
public class Book {

    @Id
    private UUID id;
    private String title;
    // getter setter ...
}

然后,我们创建一个Repository接口,其中包含指定了@Cacheable注解的方法:

public interface BookRepository extends CrudRepository<Book, UUID> {

    @Cacheable(value = "books", unless = "#a0=='Foundation'")
    Optional<Book> findFirstByTitle(String title);
}

此处的unless参数不是强制性的,它只是帮助我们稍后测试一些缓存未命中的场景。

另外,请注意SpEL表达式“#a0”,而不是更可读的“#title”。 我们这样做是因为代理不会保留参数名称。因此,我们使用替代的#root.arg[0]、p0或a0表示法。

3. 测试

我们测试的目标是确保缓存机制有效,因此这里不过多讲解Spring Data Repository实现或持久层方面。

3.1 Spring Boot

让我们从一个简单的Spring Boot测试开始。

首先,我们注入测试依赖bean,添加一些测试数据,并编写一个简单的工具方法来检查一个Book对象是否在缓存中:


@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = CacheApplication.class)
@EntityScan(basePackageClasses = Book.class)
@EnableJpaRepositories(basePackageClasses = BookRepository.class)
class BookRepositoryIntegrationTest {

    @Autowired
    CacheManager cacheManager;

    @Autowired
    private BookRepository bookRepository;

    @BeforeEach
    void setUp() {
        bookRepository.save(new Book(UUID.randomUUID(), "Dune"));
        bookRepository.save(new Book(UUID.randomUUID(), "Foundation"));
    }

    private Optional<Book> getCacheBook(String title) {
        return ofNullable(cacheManager.getCache("books")).map(c -> c.get(title, Book.class));
    }
}

现在,我们确保在请求获取一个Book对象后,它会被放入缓存中

class BookRepositoryIntegrationTest {

    @Test
    void givenBookThatShouldBeCached_whenFindByTitle_thenResultShouldBePutInCache() {
        Optional<Book> dune = bookRepository.findFirstByTitle("Dune");
        assertEquals(dune, getCacheBook("Dune"));
    }
}

另外,在某些情况下(title为Foundation)Book对象不会放入缓存中

class BookRepositoryIntegrationTest {

    @Test
    void givenBookThatShouldNotBeCached_whenFindByTitle_thenResultShouldNotBePutInCache() {
        bookRepository.findFirstByTitle("Foundation");
        assertEquals(empty(), getCacheBook("Foundation"));
    }
}

在这个测试中,我们使用Spring提供的CacheManager, 并根据@Cacheable规则检查每次bookRepository.findFirstByTitle操作后CacheManager是否包含(或不包含)Book对象

3.2 Spring

为了改变我们的Spring集成测试,这次我们mock BookRepository接口,然后我们在不同的测试用例中验证与它的交互。

首先创建一个@Configuration,它为我们的BookRepository提供mock实现:


@ContextConfiguration
@ExtendWith(SpringExtension.class)
class BookRepositoryCachingIntegrationTest {

    private static final Book DUNE = new Book(UUID.randomUUID(), "Dune");
    private static final Book FOUNDATION = new Book(UUID.randomUUID(), "Foundation");

    private BookRepository mock;

    @Autowired
    private BookRepository bookRepository;

    @EnableCaching
    @Configuration
    public static class CachingTestConfig {

        @Bean
        public BookRepository bookRepositoryMockImplementation() {
            return mock(BookRepository.class);
        }

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

在继续设置我们的mock行为之前,有两个方面值得一提的是:

  • BookRepository是我们mock的代理。因此,为了使用Mockito verify,我们通过AopTestUtils.getTargetObject检索实际的mock。
  • 我们确保在测试之间调用reset(mock),因为CachingTestConfig只加载一次。
class BookRepositoryCachingIntegrationTest {

    @BeforeEach
    void setUp() {
        mock = AopTestUtils.getTargetObject(bookRepository);
        reset(mock);

        when(mock.findFirstByTitle(eq("Foundation"))).thenReturn(of(FOUNDATION));
        when(mock.findFirstByTitle(eq("Dune")))
                .thenReturn(of(DUNE))
                .thenThrow(new RuntimeException("Book should be cached!"));
    }
}

现在,我们可以添加我们的测试方法。 首先,我们需要确保一个Book对象被放入缓存后,当稍后尝试检索该Book对象时,不应该再与Repository实现交互

class BookRepositoryCachingIntegrationTest {

    @Test
    void givenCacheBook_whenFindByTitle_thenRepositoryShouldNotBeHit() {
        assertEquals(of(DUNE), bookRepository.findFirstByTitle("Dune"));
        verify(mock).findFirstByTitle("Dune");

        assertEquals(of(DUNE), bookRepository.findFirstByTitle("Dune"));
        assertEquals(of(DUNE), bookRepository.findFirstByTitle("Dune"));
        verifyNoMoreInteractions(mock);
    }
}

此外,对于没资格缓存的Book对象,我们每次都需要调用Repository

class BookRepositoryCachingIntegrationTest {

    @Test
    void givenNotCachedBook_whenFindByTitle_thenRepositoryShouldBeHit() {
        assertEquals(of(FOUNDATION), bookRepository.findFirstByTitle("Foundation"));
        assertEquals(of(FOUNDATION), bookRepository.findFirstByTitle("Foundation"));
        assertEquals(of(FOUNDATION), bookRepository.findFirstByTitle("Foundation"));

        verify(mock, times(3)).findFirstByTitle("Foundation");
    }
}

4. 总结

综上所述,我们使用Spring、Mockito和Spring Boot实现了一系列集成测试,以确保应用于接口的缓存机制正常工作。

请注意,我们也可以结合上述方法。例如,我们可以在Spring Boot中使用mock或在普通Spring测试中对CacheManager执行检查。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章