¿Cómo probar los repositorios de Spring Data?

136

Quiero un repositorio (digamos UserRepository) creado con la ayuda de Spring Data. Soy nuevo en spring-data (pero no en spring) y uso este tutorial . Mi elección de tecnologías para tratar con la base de datos es JPA 2.1 e Hibernate. El problema es que no tengo idea de cómo escribir pruebas unitarias para dicho repositorio.

Tomemos el create()método por ejemplo. Como estoy trabajando primero en la prueba, se supone que debo escribir una prueba unitaria para ello, y ahí es donde me encuentro con tres problemas:

  • Primero, ¿cómo puedo inyectar un simulacro EntityManageren la implementación no existente de una UserRepositoryinterfaz? Spring Data generaría una implementación basada en esta interfaz:

    public interface UserRepository extends CrudRepository<User, Long> {}

    Sin embargo, no sé cómo obligarlo a usar un EntityManagersimulacro y otros simulacros; si hubiera escrito la implementación yo mismo, probablemente tendría un método de establecimiento para EntityManagerpermitirme usar mi simulacro para la prueba de la unidad. (En cuanto a la conectividad de base de datos real, tengo una JpaConfigurationclase, con la anotación @Configurationy @EnableJpaRepositories, que define programación frijoles para DataSource, EntityManagerFactory, EntityManageretc - pero repositorios deberán ser comprobadas amable y permitir la redefinición de estas cosas).

  • En segundo lugar, ¿debo probar las interacciones? Es difícil para mí descubrir qué métodos EntityManagery qué Queryse supone que se deben llamar (similar a eso 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é, no se supone que el código de la biblioteca de terceros sea probado por la unidad, solo el código que los desarrolladores escriben ellos mismos debe ser probado por la unidad. Pero si eso es cierto, todavía trae la primera pregunta a la escena: digamos, tengo un par de métodos personalizados para mi repositorio, para lo cual escribiré la implementación, ¿cómo puedo inyectar mis simulacros EntityManagery Querygenerarlos? ¿repositorio?

Nota: Probaré mis repositorios utilizando tanto la integración como las pruebas unitarias. Para mis pruebas de integración, estoy usando una base de datos en memoria HSQL, y obviamente no estoy usando una base de datos para pruebas unitarias.

Y probablemente la cuarta pregunta, ¿es correcto probar la creación correcta del gráfico de objeto y la recuperación del gráfico de objeto en las pruebas de integración (por ejemplo, tengo un gráfico de objeto complejo definido con Hibernate)?

Actualización: hoy he seguido experimentando con la 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 el 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
usuario1797032
fuente

Respuestas:

118

tl; dr

Para abreviar, no hay forma de probar unitariamente los repositorios Spring Data JPA de manera razonable por una simple razón: es engorroso burlarse de todas las partes de la API JPA que invocamos para arrancar los repositorios. Las pruebas unitarias no tienen mucho sentido aquí de todos modos, ya que generalmente no está escribiendo ningún código de implementación usted mismo (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 bastantes validaciones y configuraciones iniciales para asegurarnos de que solo pueda iniciar una aplicación que no tenga consultas derivadas no válidas, etc.

  • Creamos y almacenamos en caché CriteriaQueryinstancias para consultas derivadas para asegurarnos de que los métodos de consulta no contengan ningún error tipográfico. Esto requiere trabajar con la API Criteria así como con el meta.model.
  • Verificamos las consultas definidas manualmente al pedirles EntityManagerque creen una Queryinstancia para ellas (lo que activa efectivamente la validación de la sintaxis de las consultas).
  • Inspeccionamos los Metamodelmetadatos sobre los tipos de dominio manejados para preparar nuevas comprobaciones, etc.

Todo lo que probablemente diferiría en un repositorio escrito a mano que podría hacer que la aplicación se rompa en tiempo de ejecución (debido a consultas no válidas, etc.).

Si lo piensa, no hay código que escriba para sus repositorios, por lo que no es necesario escribir ninguna prueba unitaria . Simplemente no es necesario, ya que puede confiar en nuestra base de pruebas para detectar errores básicos (si aún se encuentra con uno, no dude en levantar un boleto ). Sin embargo, definitivamente es necesario realizar pruebas de integración para probar dos aspectos de su capa de persistencia, ya que son los aspectos relacionados con su dominio:

  • asignaciones 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 mediante el uso de una base de datos en memoria y casos de prueba que arrancan un Spring ApplicationContextgeneralmente a través del marco de contexto de prueba (como ya lo hace), rellenan previamente la base de datos (insertando instancias de objetos a través del EntityManagerrepositorio o a través de un plano Archivo SQL) y luego ejecute los métodos de consulta para verificar el resultado de ellos.

Probar implementaciones personalizadas

Las partes de implementación personalizadas del repositorio están escritas de manera que no tengan que saber sobre Spring Data JPA. Son frijoles simples de primavera que se EntityManagerinyectan. Por supuesto, es posible que desee tratar de burlarse de las interacciones con él, pero para ser honesto, probar la unidad de JPA no ha sido una experiencia demasiado agradable para nosotros y funciona con muchas indirecciones ( EntityManager-> CriteriaBuilder, CriteriaQueryetc.) que terminas con simulacros que devuelven simulacros, etc.

Oliver Drotbohm
fuente
55
¿Tiene un enlace a un pequeño ejemplo de una prueba de integración con una base de datos en memoria (por ejemplo, h2)?
Wim Deblauwe
77
Los ejemplos aquí usan HSQLDB. Cambiar a H2 es básicamente una cuestión de intercambiar la dependencia en el pom.xml.
Oliver Drotbohm
3
Gracias, pero esperaba ver un ejemplo que rellene previamente la base de datos y / o realmente compruebe la base de datos.
Wim Deblauwe
1
El enlace detrás de "escrito de alguna manera" ya no funciona. ¿Quizás puedas actualizarlo?
Wim Deblauwe
1
Entonces, ¿propone utilizar pruebas de integración en lugar de pruebas unitarias para implementaciones personalizadas también? ¿Y no escribir pruebas unitarias para ellos? Solo para aclarar. Está bien si es así. Entiendo la razón (demasiado compleja para burlarse de todas las cosas). Soy nuevo en las pruebas de JPA, así que solo quiero resolverlo.
Ruslan Stelmachenko
48

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 muestra 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 una en el classpath.

Markus T
fuente
77
Esta es una prueba de integración , no una prueba unitaria que OP mencionó
Iwo Kucharski el
16
@IwoKucharski. Tienes razón sobre la terminología. Sin embargo: dado que Spring Data implementa la interfaz para usted, es difícil usar Spring, y en ese momento se convierte en una prueba de integración. Si hice una pregunta como esta, probablemente también solicité una prueba unitaria sin pensar en la terminología. Por lo tanto, no lo vi como el punto principal o incluso central de la pregunta.
Markus T
@RunWith(SpringRuner.class)ahora ya está incluido en el @DataJpaTest.
Maroun
@IwoKucharski, ¿por qué esta es una prueba de integración, no una prueba unitaria?
user1182625
@ user1182625 @RunWith(SpringRunner.classinicia el contexto de primavera, lo que significa que está comprobando la integración entre varias unidades. Prueba de unidad es probar una sola unidad -> clase única. Luego escribe MyClass sut = new MyClass();y prueba el objeto sut (sut = servicio bajo prueba)
Iwo Kucharski
21

Esto puede llegar un poco tarde, pero he escrito algo para este mismo propósito. Mi biblioteca se burlará de los métodos básicos de repositorio crud para usted e interpretará la mayoría de las funcionalidades de sus métodos de consulta. Tendrá que inyectar funcionalidades para sus propias consultas nativas, pero el resto lo hará por usted.

Echar un vistazo:

https://github.com/mmnaseri/spring-data-mock

ACTUALIZAR

Esto está ahora en Maven central y en muy buena forma.

Milad Naseri
fuente
16

Si está usando Spring Boot, simplemente puede usar @SpringBootTestpara cargar en su ApplicationContext(que es lo que le está ladrando su stacktrace). Esto le permite conectar automáticamente en sus repositorios de datos de primavera. Asegúrese de agregar @RunWith(SpringRunner.class)para que se recojan las anotaciones específicas del resorte:

@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 documentos .

heez
fuente
Este es un ejemplo bastante bueno, pero simplista en mi opinión. ¿Hay alguna situación en la que esta prueba pueda fallar?
HopeKing
No este en sí, pero supongamos que desea probar Predicates (que era mi caso de uso) funciona bastante bien.
heez
1
para mí el repositorio siempre es nulo. ¿Alguna ayuda?
Atul Chaudhary
Esta es mi mejor respuesta. De esta manera, prueba los scripts CrudRepo, Entity y DDL que crean las tablas de la Entidad.
MirandaVeracruzDeLaHoyaCardina
He escrito una prueba exactamente como esta. Funciona perfectamente cuando la implementación del repositorio utiliza jdbcTemplate. Sin embargo, cuando cambio la implementación de spring-data (al extender la interfaz desde Repository), la prueba falla y userRepository.findOne devuelve nulo. ¿Alguna idea de cómo resolver esto?
Rega
8

En la última versión de spring boot 2.1.1.RELEASE , es simple como:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SampleApplication.class)
public class CustomerRepositoryIntegrationTest {

    @Autowired
    CustomerRepository repository;

    @Test
    public void myTest() throws Exception {

        Customer customer = new Customer();
        customer.setId(100l);
        customer.setFirstName("John");
        customer.setLastName("Wick");

        repository.save(customer);

        List<?> queryResult = repository.findByLastName("Wick");

        assertFalse(queryResult.isEmpty());
        assertNotNull(queryResult.get(0));
    }
}

Código completo:

https://github.com/jrichardsz/spring-boot-templates/blob/master/003-hql-database-with-integration-test/src/test/java/test/CustomerRepositoryIntegrationTest.java

JRichardsz
fuente
3
Este es un 'ejemplo' bastante incompleto: no se puede construir, las pruebas de "integración" usan la misma configuración que el código de producción. Es decir. bueno para nada.
Martin Mucha
Me disculpo. Me azotaré por este error. Por favor intente una vez más!
JRichardsz
Esto también funciona con 2.0.0.RELEASESpring Boot.
Nital
Debería usar db incrustado para esta prueba
TuGordoBello
7

Cuando realmente desee escribir una prueba i para un repositorio de datos de Spring, puede hacerlo así:

@RunWith(SpringRunner.class)
@DataJpaTest
@EnableJpaRepositories(basePackageClasses = WebBookingRepository.class)
@EntityScan(basePackageClasses = WebBooking.class)
public class WebBookingRepositoryIntegrationTest {

    @Autowired
    private WebBookingRepository repository;

    @Test
    public void testSaveAndFindAll() {
        WebBooking webBooking = new WebBooking();
        webBooking.setUuid("some uuid");
        webBooking.setItems(Arrays.asList(new WebBookingItem()));
        repository.save(webBooking);

        Iterable<WebBooking> findAll = repository.findAll();

        assertThat(findAll).hasSize(1);
        webBooking.setId(1L);
        assertThat(findAll).containsOnly(webBooking);
    }
}

Para seguir este ejemplo, debe usar estas dependencias:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.197</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.9.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
Philipp Wirth
fuente
5

Resolví esto usando de esta manera:

    @RunWith(SpringRunner.class)
    @EnableJpaRepositories(basePackages={"com.path.repositories"})
    @EntityScan(basePackages={"com.model"})
    @TestPropertySource("classpath:application.properties")
    @ContextConfiguration(classes = {ApiTestConfig.class,SaveActionsServiceImpl.class})
    public class SaveCriticalProcedureTest {

        @Autowired
        private SaveActionsService saveActionsService;
        .......
        .......
}
Ajay Kumar
fuente
4

Con JUnit5 y la @DataJpaTestprueba se verá así (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))
    }
}

Se podría utilizar TestEntityManagerde org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerpaquete con el fin de validar estado de entidad.

Przemek Nowak
fuente
Siempre es mejor generar ID para el bean de entidad.
Arundev
Para Java, la segunda línea es: @ExtendWith (value = SpringExtension.class)
AdilOoze