unit testing - test - ¿Cómo probar los repositorios de Spring Data?
spring unit test (5)
Quiero un repositorio (por ejemplo, UserRepository
) creado con la ayuda de Spring Data. Soy nuevo en los datos de primavera (pero no en primavera) y utilizo este tutorial . Mi elección de tecnologías para manejar la base de datos es JPA 2.1 e Hibernate. El problema es que no tengo ni idea de cómo escribir pruebas unitarias para ese repositorio.
Tomemos el método create()
por ejemplo. Como estoy trabajando primero en las pruebas, se supone que debo escribir una prueba unitaria para ello, y ahí es donde me encuentro con tres problemas:
En primer lugar, ¿cómo puedo inyectar un simulacro de un
EntityManager
en la implementación no existente de una interfazUserRepository
? Spring Data generaría una implementación basada en esta interfaz:public interface UserRepository extends CrudRepository<User, Long> {}
Sin embargo, no sé cómo forzarlo a usar un simulacro de
EntityManager
y otros simulacros: si yo mismo hubiera escrito la implementación, probablemente tendría un método deEntityManager
paraEntityManager
, que me permitiera usar mi simulacro para la prueba unitaria. (En cuanto a la conectividad de la base de datos real, tengo una claseJpaConfiguration
, anotada con@Configuration
y@EnableJpaRepositories
, que define de forma programática beans paraDataSource
,EntityManagerFactory
,EntityManager
, etc., pero los repositorios deben ser fáciles de usar y permitir anular estas cosas).En segundo lugar, ¿debería probar las interacciones? Es difícil para mí descubrir qué métodos de
EntityManager
yQuery
se deben llamar (similar a thatverify(entityManager).createNamedQuery(anyString()).getResultList();
), ya que no soy yo quien está escribiendo la implementación.En tercer lugar, ¿se supone que debo probar los métodos generados por Spring-Data en primer lugar? Como sé, el código de la biblioteca de terceros no debe ser probado por la unidad, solo se supone que el código que los desarrolladores mismos escriben debe ser probado por la unidad. Pero si eso es cierto, todavía trae la primera pregunta a la escena: por ejemplo, tengo un par de métodos personalizados para mi repositorio, para lo cual escribiré la implementación, ¿cómo puedo inyectar mis
EntityManager
deEntityManager
yQuery
en laEntityManager
final? ¿repositorio generado?
Nota: voy a probar mis repositorios usando las pruebas de integración y unidad. Para mis pruebas de integración estoy usando una base de datos HSQL en memoria, y obviamente no estoy usando una base de datos para pruebas unitarias.
Y, probablemente, la cuarta pregunta, ¿es correcto probar la correcta creación de gráficos de objetos y la recuperación de gráficos de objetos en las pruebas de integración (por ejemplo, tengo un gráfico de objetos complejo definido con Hibernate)?
Actualización: hoy he seguido experimentando con inyección simulada: he creado una clase interna estática para permitir la inyección simulada.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class UserRepositoryTest {
@Configuration
@EnableJpaRepositories(basePackages = "com.anything.repository")
static class TestConfiguration {
@Bean
public EntityManagerFactory entityManagerFactory() {
return mock(EntityManagerFactory.class);
}
@Bean
public EntityManager entityManager() {
EntityManager entityManagerMock = mock(EntityManager.class);
//when(entityManagerMock.getMetamodel()).thenReturn(mock(Metamodel.class));
when(entityManagerMock.getMetamodel()).thenReturn(mock(MetamodelImpl.class));
return entityManagerMock;
}
@Bean
public PlatformTransactionManager transactionManager() {
return mock(JpaTransactionManager.class);
}
}
@Autowired
private UserRepository userRepository;
@Autowired
private EntityManager entityManager;
@Test
public void shouldSaveUser() {
User user = new UserBuilder().build();
userRepository.save(user);
verify(entityManager.createNamedQuery(anyString()).executeUpdate());
}
}
Sin embargo, ejecutar esta prueba me da la siguiente stacktrace:
java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.DefaultTestContext.getApplicationContext(DefaultTestContext.java:101)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:109)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:75)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:319)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:212)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:232)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:175)
at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:77)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:195)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:63)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ''userRepository'': Error setting property values; nested exception is org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property ''entityManager'' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null!
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1493)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1197)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:684)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)
at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:121)
at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60)
at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:100)
at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:250)
at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContextInternal(CacheAwareContextLoaderDelegate.java:64)
at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:91)
... 28 more
Caused by: org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property ''entityManager'' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null!
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:108)
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:62)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1489)
... 44 more
tl; dr
Para abreviar, no hay manera de probar los repositorios de JPA de Spring Data de forma razonable por una simple razón: es engorroso burlarse de todas las partes de la API de JPA que invocamos para arrancar los repositorios. De todos modos, las pruebas unitarias no tienen demasiado sentido, ya que normalmente no está escribiendo ningún código de implementación (consulte el párrafo a continuación sobre implementaciones personalizadas) para que las pruebas de integración sean el enfoque más razonable.
Detalles
Realizamos una gran cantidad de validaciones y configuraciones iniciales para asegurarnos de que solo pueda arrancar una aplicación que no tenga consultas derivadas no válidas, etc.
- Creamos y almacenamos en caché las instancias de
CriteriaQuery
para las consultas derivadas para garantizar que los métodos de consulta no contengan errores tipográficos. Esto requiere trabajar con la API de Criteria, así como con el meta.model. - Verificamos las consultas definidas manualmente solicitando al
EntityManager
que cree una instancia deQuery
para esas (lo que activa la validación de la sintaxis de la consulta). - Inspeccionamos el
Metamodel
para obtener metadatos sobre los tipos de dominio manejados para preparar verificaciones nuevas, etc.
Todo lo que probablemente difiera en un repositorio escrito a mano que podría ocasionar que la aplicación se rompa en el tiempo de ejecución (debido a consultas no válidas, etc.).
Si lo piensas, no hay ningún código que escribas para tus repositorios, por lo que no es necesario escribir ninguna prueba unitaria . Simplemente no hay necesidad de hacerlo ya que puede confiar en nuestra base de pruebas para detectar errores básicos (si todavía encuentra uno, siéntase libre de subir un ticket ). Sin embargo, definitivamente hay necesidad de pruebas de integración para probar dos aspectos de su capa de persistencia, ya que son los aspectos relacionados con su dominio:
- mapeos de entidades
- semántica de consulta (la sintaxis se verifica en cada intento de arranque de todos modos).
Pruebas de integración
Esto generalmente se hace usando una base de datos en memoria y casos de prueba que arrancan un Spring ApplicationContext
generalmente a través del marco de contexto de prueba (como ya lo hace), rellena previamente la base de datos (insertando instancias de objeto a través del EntityManager
o repositorio, o vía un archivo SQL simple) y luego ejecutar los métodos de consulta para verificar el resultado de ellos.
Prueba de implementaciones personalizadas
Las partes de implementación personalizadas del repositorio están escritas de una manera que no tienen que saber acerca de Spring Data JPA. Son simples granos de primavera que gen y un EntityManager
inyectaron. Por supuesto, puede intentar burlarse de las interacciones con él, pero para ser sincero, la prueba unitaria de la JPA no ha sido una experiencia demasiado agradable para nosotros, y funciona con bastantes indirecciones ( EntityManager
-> CriteriaBuilder
, CriteriaQuery
, etc. .) para que termines con burlas volviendo burlas, etc.
Con Spring Boot + Spring Data se ha vuelto bastante fácil:
@RunWith(SpringRunner.class)
@DataJpaTest
public class MyRepositoryTest {
@Autowired
MyRepository subject;
@Test
public void myTest() throws Exception {
subject.save(new MyEntity());
}
}
La solución de @heez muestra el contexto completo, esto solo trae a colación lo que se necesita para que JPA + Transaction funcione. Tenga en cuenta que la solución anterior mostrará una base de datos de prueba en memoria dado que se puede encontrar uno en la ruta de clases.
Con la prueba JUnit5 y @DataJpaTest
se verá como (código kotlin):
@DataJpaTest
@ExtendWith(value = [SpringExtension::class])
class ActivityJpaTest {
@Autowired
lateinit var entityManager: TestEntityManager
@Autowired
lateinit var myEntityRepository: MyEntityRepository
@Test
fun shouldSaveEntity() {
// when
val savedEntity = myEntityRepository.save(MyEntity(1, "test")
// then
Assertions.assertNotNull(entityManager.find(MyEntity::class.java, savedEntity.id))
}
}
Puede usar TestEntityManager
del paquete org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
para validar el estado de la entidad.
Esto puede llegar un poco tarde, pero he escrito algo para este propósito. Mi biblioteca se burlará de los métodos básicos de depósito crud para usted, así como interpretar la mayoría de las funcionalidades de sus métodos de consulta. Tendrá que insertar funcionalidades para sus propias consultas nativas, pero el resto está hecho para usted.
Echar un vistazo:
https://github.com/mmnaseri/spring-data-mock
ACTUALIZAR
Esto ahora está en Maven central y en muy buena forma.
Si está utilizando Spring Boot, puede simplemente usar @SpringBootTest
para cargar en su ApplicationContext
(que es de lo que su stacktrace le está ladrando). Esto le permite realizar el autoenvío en sus repositorios de datos de primavera. Asegúrese de agregar @RunWith(SpringRunner.class)
para que las anotaciones específicas del resorte se @RunWith(SpringRunner.class)
:
@RunWith(SpringRunner.class)
@SpringBootTest
public class OrphanManagementTest {
@Autowired
private UserRepository userRepository;
@Test
public void saveTest() {
User user = new User("Tom");
userRepository.save(user);
Assert.assertNotNull(userRepository.findOne("Tom"));
}
}
Puede leer más sobre las pruebas en el arranque de primavera en sus docs .