unit test example annotation unit-testing jpa mockito spring-data spring-data-jpa

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 interfaz UserRepository ? 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 de EntityManager para EntityManager , que me permitiera usar mi simulacro para la prueba unitaria. (En cuanto a la conectividad de la base de datos real, tengo una clase JpaConfiguration , anotada con @Configuration y @EnableJpaRepositories , que define de forma programática beans para DataSource , 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 y Query se deben llamar (similar a that verify(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 de EntityManager y Query en la EntityManager 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 de Query 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 .