Como testar os repositórios do Spring Data?

136

Quero um repositório (digamos UserRepository) criado com a ajuda do Spring Data. Eu sou novo no Spring-Data (mas não no Spring) e uso este tutorial . Minha escolha de tecnologias para lidar com o banco de dados é JPA 2.1 e Hibernate. O problema é que não tenho noção de como escrever testes de unidade para esse repositório.

Vamos usar o create()método, por exemplo. Enquanto estou trabalhando no teste primeiro, devo escrever um teste de unidade para ele - e é aí que me deparei com três problemas:

  • Primeiro, como injetar uma simulação de uma EntityManagerimplementação inexistente de uma UserRepositoryinterface? O Spring Data geraria uma implementação baseada nesta interface:

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

    No entanto, não sei como forçá-lo a usar uma EntityManagersimulação e outras simulações - se eu tivesse escrito a implementação, provavelmente teria um método setter EntityManager, permitindo que eu usasse a simulação para o teste de unidade. (Quanto à conectividade banco de dados real, eu tenho uma JpaConfigurationclasse, anotado com @Configuratione @EnableJpaRepositories, que define programaticamente feijão para DataSource, EntityManagerFactory, EntityManageretc. - mas repositórios deve ser amigável-teste e permitem substituir essas coisas).

  • Segundo, devo testar interações? É difícil para mim descobrir quais métodos EntityManagere Querycomo devem ser chamados (semelhante a isso verify(entityManager).createNamedQuery(anyString()).getResultList();), pois não sou eu quem está escrevendo a implementação.

  • Terceiro, devo testar os métodos gerados pelos dados da Spring em primeiro lugar? Como eu sei, o código da biblioteca de terceiros não deve ser testado em unidade - apenas o código que os desenvolvedores escrevem é testado em unidade. Mas se isso é verdade, ele ainda traz a primeira volta pergunta para a cena: dizer, eu tenho um par de métodos personalizados para meu repositório, para o qual eu estarei escrevendo implementação, como faço para injetar meus simulações de EntityManagere Querypara a final, gerados repositório?

Nota: Eu estarei de test-drive meus repositórios usando tanto a integração e os testes de unidade. Para meus testes de integração, estou usando um banco de dados HSQL na memória e obviamente não estou usando um banco de dados para testes de unidade.

E provavelmente a quarta pergunta, é correto testar a criação e a recuperação corretas de gráfico de objeto nos testes de integração (por exemplo, eu tenho um gráfico de objeto complexo definido com o Hibernate)?

Atualização: hoje continuo experimentando a injeção simulada - criei uma classe interna estática para permitir a injeção 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());
}

}

No entanto, a execução desse teste fornece o seguinte rastreamento de pilha:

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
user1797032
fonte

Respostas:

118

tl; dr

Para resumir - não há como testar os repositórios JPA do Spring Data razoavelmente por uma razão simples: é muito complicado zombar de todas as partes da API JPA que invocamos para inicializar os repositórios. Os testes de unidade não fazem muito sentido aqui, pois você normalmente não está escrevendo nenhum código de implementação (veja o parágrafo abaixo sobre implementações personalizadas) para que o teste de integração seja a abordagem mais razoável.

Detalhes

Realizamos várias validações e configurações iniciais para garantir que você possa inicializar apenas um aplicativo que não tenha consultas derivadas inválidas etc.

  • Criamos e armazenamos em cache CriteriaQueryinstâncias para consultas derivadas para garantir que os métodos de consulta não contenham erros de digitação. Isso requer o trabalho com a API Criteria e com o meta.model.
  • Verificamos as consultas definidas manualmente solicitando a EntityManagercriação de uma Queryinstância para elas (o que efetivamente aciona a validação da sintaxe da consulta).
  • Inspecionamos os Metamodelmetadados sobre os tipos de domínio manipulados para preparar novas verificações, etc.

Tudo o que você provavelmente adiaria em um repositório escrito à mão que poderia causar a quebra do aplicativo em tempo de execução (devido a consultas inválidas etc.).

Se você pensar bem, não há código que você escreve para seus repositórios; portanto, não há necessidade de escrever nenhum teste de unidade . Simplesmente não é necessário, pois você pode confiar em nossa base de testes para detectar erros básicos (se você ainda encontrar um deles, sinta-se à vontade para pedir uma multa ). No entanto, definitivamente existem testes de integração para testar dois aspectos da sua camada de persistência, pois são os aspectos relacionados ao seu domínio:

  • mapeamentos de entidade
  • semântica de consulta (sintaxe é verificada em cada tentativa de inicialização) de qualquer maneira).

Testes de integração

Isso geralmente é feito usando um banco de dados na memória e casos de teste que iniciam um Spring ApplicationContextnormalmente através da estrutura de contexto de teste (como você já faz), preenchem previamente o banco de dados (inserindo instâncias de objeto por meio do EntityManagerrepositório ou repo ou por meio de uma planilha Arquivo SQL) e, em seguida, execute os métodos de consulta para verificar o resultado deles.

Testando implementações personalizadas

Partes de implementação personalizadas do repositório são escritas de uma maneira que eles não precisam saber sobre o Spring Data JPA. São feijões comuns da Primavera que são EntityManagerinjetados. É claro que você pode tentar zombar das interações com ele, mas, para ser sincero, testar a JPA por unidade não foi uma experiência muito agradável para nós, além de funcionar com muitos indiretos ( EntityManager-> CriteriaBuilder, CriteriaQueryetc.). que você acaba com zombarias retornando zombarias e assim por diante.

Oliver Drotbohm
fonte
5
Você tem um link para um pequeno exemplo de teste de integração com um banco de dados na memória (por exemplo, h2)?
Wim Deblauwe
7
Os exemplos aqui usam HSQLDB. Mudar para H2 é basicamente uma questão de trocar a dependência no pom.xml.
Oliver Drotbohm
3
Obrigado, mas eu esperava ver um exemplo que preenche previamente o banco de dados e / ou realmente verifique o banco de dados.
Wim Deblauwe
1
O link por trás de "escrito de uma maneira" não funciona mais. Talvez você possa atualizá-lo?
Wim Deblauwe
1
Então, você propõe usar testes de integração em vez de testes de unidade para implementações personalizadas também? E não escrever testes de unidade para eles? Só para esclarecer. Tudo bem se sim. Eu entendo o motivo (complexo demais para zombar de todas as coisas). Eu sou novo no teste de JPA, então só quero descobrir.
Ruslan Stelmachenko
48

Com o Spring Boot + Spring Data, tornou-se bastante fácil:

@RunWith(SpringRunner.class)
@DataJpaTest
public class MyRepositoryTest {

    @Autowired
    MyRepository subject;

    @Test
    public void myTest() throws Exception {
        subject.save(new MyEntity());
    }
}

A solução da @heez traz o contexto completo, apenas o necessário para que o JPA + Transaction funcione. Observe que a solução acima exibirá um banco de dados de teste na memória, pois esse pode ser encontrado no caminho de classe.

Markus T
fonte
7
Esta é uma integração de teste, não unidade de teste que OP mencionado
Iwo Kucharski
16
@IwoKucharski. Você está certo sobre a terminologia. No entanto: como o Spring Data implementa a interface para você, é difícil usar o Spring e, nesse momento, ele se torna um teste de integração. Se eu fizesse uma pergunta como essa, provavelmente também solicitei um teste de unidade sem pensar na terminologia. Portanto, não vi isso como o principal, ou mesmo o principal, ponto da questão.
Markus T
@RunWith(SpringRuner.class)já está incluído no @DataJpaTest.
Maroun 23/01
@IwoKucharski, por que isso é teste de integração, não teste de unidade?
user1182625 12/06
@ user1182625 @RunWith(SpringRunner.classinicia o contexto da mola, o que significa que está verificando a integração entre várias unidades. O teste de unidade está testando uma única unidade -> classe única. Então você escreve MyClass sut = new MyClass();e testa o objeto sut (sut = serviço em teste)
Iwo Kucharski
21

Isso pode ser um pouco tarde demais, mas escrevi algo para esse fim. Minha biblioteca zombará dos métodos básicos do repositório crud para você e interpretará a maioria das funcionalidades dos seus métodos de consulta. Você precisará injetar funcionalidades para suas próprias consultas nativas, mas o resto é feito para você.

Dê uma olhada:

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

ATUALIZAR

Isso agora está no Maven central e em muito boa forma.

Milad Naseri
fonte
16

Se você estiver usando o Spring Boot, pode simplesmente usar @SpringBootTestpara carregar o seu ApplicationContext(que é o que o seu stacktrace está latindo para você). Isso permite que você conecte automaticamente seus repositórios de dados de primavera. Certifique-se de adicionar @RunWith(SpringRunner.class)para que as anotações específicas da mola sejam selecionadas:

@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"));
  }
}

Você pode ler mais sobre os testes na inicialização por mola nos documentos deles .

heez
fonte
Este é um exemplo bastante bom, mas simplista na minha opinião. Existem situações em que esse teste pode até falhar?
HopeKing
Não este por si só, mas suponha que você queira testar Predicates (que foi o meu caso de uso) funciona muito bem.
heez
1
para mim, o repositório é sempre nulo. Qualquer ajuda?
Atul Chaudhary
Esta é a melhor resposta. Dessa maneira, você testa os scripts CrudRepo, Entity e DDL, que criam as tabelas da Entity.
MirandaVeracruzDeLaHoyaCardina 25/09
Eu escrevi um teste exatamente como este. Funciona perfeitamente quando a implementação do Repositório utiliza jdbcTemplate. No entanto, quando altero a implementação dos dados da primavera (estendendo a interface do Repository), o teste falha e o userRepository.findOne retorna nulo. Alguma idéia de como resolver isso?
Rega
8

Na última versão do boot inicial 2.1.1.RELEASE , é simples 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
fonte
3
Este é um "exemplo" bastante incompleto: não pode ser construído, os testes de "integração" usam a mesma configuração do código de produção. Ou seja. bom para nada.
Martin Mucha
Peço desculpas. Vou me chicotear por causa desse erro. Por favor, tente mais uma vez!
JRichardsz 13/02/19
Isso também funciona com o 2.0.0.RELEASESpring Boot.
Nital
Você deve usar o banco de dados incorporado para este teste
TuGordoBello
7

Quando você realmente deseja escrever um i-test para um repositório de dados do Spring, pode fazê-lo assim:

@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 exemplo, você precisa usar estas dependências:

<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
fonte
5

Eu resolvi isso usando desta maneira -

    @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
fonte
4

Com o JUnit5 e o @DataJpaTestteste será parecido com (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))
    }
}

Você pode usar TestEntityManagerdo org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerpacote para validar o estado da entidade.

Przemek Nowak
fonte
É sempre melhor gerar ID para o bean de entidade.
Arundev
Para Java, a segunda linha é: @ExtendWith (value = SpringExtension.class)
AdilOoze