mvc - ¿Cómo probar el soporte de almacenamiento en caché declarativo de Spring en los repositorios de Spring Data?
spring mvc tag select (2)
He desarrollado un repositorio de Spring Data, la interfaz MemberRepository
, que extiende org.springframework.data.jpa.repository.JpaRepository
. MemberRepository
tiene un método:
@Cacheable(CacheConfiguration.DATABASE_CACHE_NAME)
Member findByEmail(String email);
El resultado se almacena en caché mediante la abstracción de la memoria caché de Spring (respaldada por un ConcurrentMapCache
).
El problema que tengo es que quiero escribir una prueba de integración (contra hsqldb) que afirme que el resultado se recupera de la primera vez que se encuentra en la base de datos y de la caché la segunda vez .
Inicialmente pensé en burlarme de la infraestructura de jpa (administrador de entidades, etc.) y afirmar de alguna manera que no se llama al administrador de entidades la segunda vez, pero parece demasiado difícil / engorroso (consulte https://stackoverflow.com/a/23442457/536299 ).
¿Alguien puede proporcionar consejos sobre cómo probar el comportamiento de almacenamiento en caché de un método de Spring Data Repository anotado con @Cacheable
?
Intenté probar el comportamiento del caché en mi aplicación usando el ejemplo de Oliver. En mi caso, mi caché está configurado en la capa de servicio y quiero verificar que mi repositorio se esté llamando el número correcto de veces. Estoy usando burlas spock en lugar de mockito. Pasé un tiempo tratando de averiguar por qué fallan mis pruebas, hasta que me di cuenta de que las pruebas que se ejecutan primero llenan el caché y afectan a las otras pruebas. Después de borrar la memoria caché para cada prueba, comenzaron a comportarse como se esperaba.
Esto es lo que terminé con:
@ContextConfiguration
class FooBarServiceCacheTest extends Specification {
@TestConfiguration
@EnableCaching
static class Config {
def mockFactory = new DetachedMockFactory()
def fooBarRepository = mockFactory.Mock(FooBarRepository)
@Bean
CacheManager cacheManager() {
new ConcurrentMapCacheManager(FOOBARS)
}
@Bean
FooBarRepository fooBarRepository() {
fooBarRepository
}
@Bean
FooBarService getFooBarService() {
new FooBarService(fooBarRepository)
}
}
@Autowired
@Subject
FooBarService fooBarService
@Autowired
FooBarRepository fooBarRepository
@Autowired
CacheManager cacheManager
def "setup"(){
// we want to start each test with an new cache
cacheManager.getCache(FOOBARS).clear()
}
def "should return cached foobars "() {
given:
final foobars = [new FooBar(), new FooBar()]
when:
fooBarService.getFooBars()
fooBarService.getFooBars()
final fooBars = fooBarService.getFooBars()
then:
1 * fooBarRepository.findAll() >> foobars
}
def "should return new foobars after clearing cache"() {
given:
final foobars = [new FooBar(), new FooBar()]
when:
fooBarService.getFooBars()
fooBarService.clearCache()
final fooBars = fooBarService.getFooBars()
then:
2 * fooBarRepository.findAll() >> foobars
}
}
Si desea probar un aspecto técnico como el almacenamiento en caché, no utilice una base de datos. Es importante entender lo que te gustaría probar aquí. Desea asegurarse de que se evite la invocación del método para la invocación con los mismos argumentos. El repositorio al frente de una base de datos es un aspecto completamente ortogonal de este tema.
Esto es lo que yo recomendaría:
- Configure una prueba de integración que configure el almacenamiento en caché declarativo (o importe los bits y piezas necesarios de su configuración de producción.
- Configure una instancia simulada de su repositorio.
- Escriba un caso de prueba para configurar el comportamiento esperado del simulacro, invoque los métodos y verifique la salida en consecuencia.
Muestra
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class CachingIntegrationTest {
// Your repository interface
interface MyRepo extends Repository<Object, Long> {
@Cacheable("sample")
Object findByEmail(String email);
}
@Configuration
@EnableCaching
static class Config {
// Simulating your caching configuration
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager("sample");
}
// A repository mock instead of the real proxy
@Bean
MyRepo myRepo() {
return Mockito.mock(MyRepo.class);
}
}
@Autowired CacheManager manager;
@Autowired MyRepo repo;
@Test
public void methodInvocationShouldBeCached() {
Object first = new Object();
Object second = new Object();
// Set up the mock to return *different* objects for the first and second call
Mockito.when(repo.findByEmail(Mockito.any(String.class))).thenReturn(first, second);
// First invocation returns object returned by the method
Object result = repo.findByEmail("foo");
assertThat(result, is(first));
// Second invocation should return cached value, *not* second (as set up above)
result = repo.findByEmail("foo");
assertThat(result, is(first));
// Verify repository method was invoked once
Mockito.verify(repo, Mockito.times(1)).findByEmail("foo");
assertThat(manager.getCache("sample").get("foo"), is(notNullValue()));
// Third invocation with different key is triggers the second invocation of the repo method
result = repo.findByEmail("bar");
assertThat(result, is(second));
}
}
Como puede ver, hacemos un poco de pruebas excesivas aquí:
- El control más relevante, creo, es que la segunda llamada devuelve el primer objeto. De eso se trata el caché. Las dos primeras llamadas con la misma clave devuelven el mismo objeto, mientras que la tercera llamada con una clave diferente da como resultado la segunda invocación real en el repositorio.
- Reforzamos el caso de prueba al verificar que la memoria caché tenga un valor para la primera clave. Incluso se podría extender eso para verificar el valor real. Por otro lado, también creo que está bien evitar hacerlo ya que tiende a probar más los mecanismos internos del mecanismo en lugar del comportamiento a nivel de la aplicación.
Clave para llevar
- No necesita ninguna infraestructura para probar el comportamiento del contenedor.
- Configurar un caso de prueba es sencillo y sencillo.
- Los componentes bien diseñados le permiten escribir casos de prueba simples y requieren menos trabajo de integración para realizar la prueba.