如何测试Spring Data存储库?

use*_*032 119 unit-testing jpa spring-data spring-data-jpa

我想要一个UserRepository在Spring Data的帮助下创建的存储库(比方说).我是spring-data(但不是spring)的新手,我使用的是本教程.我选择处理数据库的技术是JPA 2.1和Hibernate.问题是我对如何为这样的存储库编写单元测试一无所知.

我们以create()方法为例.当我正在测试时,我应该为它编写单元测试 - 这就是我遇到的三个问题:

  • 首先,如何将一个模拟注入EntityManager一个UserRepository接口的不存在的实现?Spring Data将基于此接口生成实现:

    public interface UserRepository extends CrudRepository<User, Long> {}
    
    Run Code Online (Sandbox Code Playgroud)

    但是,我不知道如何强制它使用EntityManager模拟和其他模拟 - 如果我自己编写了实现,我可能会有一个setter方法EntityManager,允许我使用我的模拟进行单元测试.(至于实际的数据库连接,我有一个JpaConfiguration类,有注释@Configuration@EnableJpaRepositories,通过编程定义豆类DataSource,EntityManagerFactory,EntityManager等等-但库应该是测试友好,并允许重写这些事情).

  • 其次,我应该测试互动吗?这是我很难找出什么方法EntityManagerQuery应该被称为(类似于那个verify(entityManager).createNamedQuery(anyString()).getResultList();),因为它是不是我是谁写的实施.

  • 第三,我是否应该首先对Spring-Data生成的方法进行单元测试?据我所知,第三方库代码不应该进行单元测试 - 只有开发人员自己编写的代码应该进行单元测试.但如果这是真的,它仍然会将第一个问题带回现场:比方说,我的存储库有几个自定义方法,我将编写实现,如何注入我的模拟EntityManagerQuery进入最终,生成库?

注:我将使用测试驱动我的仓库集成和单元测试.对于我的集成测试,我使用的是HSQL内存数据库,显然我没有使用数据库进行单元测试.

也许第四个问题,在集成测试中测试正确的对象图创建和对象图检索是否正确(比方说,我有一个用Hibernate定义的复杂对象图)?

更新:今天我继续尝试模拟注入 - 我创建了一个静态内部类来允许模拟注入.

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

}
Run Code Online (Sandbox Code Playgroud)

但是,运行此测试会给我以下堆栈跟踪:

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
Run Code Online (Sandbox Code Playgroud)

Oli*_*ohm 104

TL;博士

简而言之 - 由于一个简单的原因,无法合理地对Spring Data JPA存储库进行单元测试:模拟我们调用以引导存储库的JPA API的所有部分是很麻烦的.无论如何,单元测试在这里没有太大意义,因为您通常不会自己编写任何实现代码(请参阅下面关于自定义实现的段落),以便集成测试是最合理的方法.

细节

我们进行了大量的前期验证和设置,以确保您只能引导没有无效派生查询的应用程序等.

  • 我们CriteriaQuery为派生查询创建和缓存实例,以确保查询方法不包含任何拼写错误.这需要使用Criteria API以及meta.model.
  • 我们通过要求为这些查询EntityManager创建Query实例来验证手动定义的查询(这有效地触发了查询语法验证).
  • 我们检查Metamodel有关处理的域类型的元数据,以准备新的检查等.

您可能在手写存储库中推迟的所有内容都可能导致应用程序在运行时中断(由于查询无效等).

如果你考虑一下,你没有为你的存储库编写代码,所以不需要编写任何单元测试.根本没有必要,因为你可以依靠我们的测试基地来捕获基本的错误(如果你碰巧碰到一个,可以随意提出一张).但是,肯定需要集成测试来测试持久层的两个方面,因为它们是与您的域相关的方面:

  • 实体映射
  • 查询语义(无论如何都会在每次引导尝试时验证语法).

集成测试

这通常通过使用内存数据库和测试用例ApplicationContext来完成,这些测试用例通常通过测试上下文框架(正如您已经做过)引导Spring ,预先填充数据库(通过插件EntityManager或repo 插入对象实例,或通过平面插入SQL文件)然后执行查询方法来验证它们的结果.

测试自定义实现

存储库的自定义实现部分是以他们不必了解Spring Data JPA 的方式编写的.它们是普通的春豆,可以EntityManager注入.当然想,你可以尝试用它来模拟的互动,但说实话,单元测试的JPA一直不是我们太愉快的经历,以及它的工作原理与相当多的间接性(EntityManager- > CriteriaBuilder,CriteriaQuery等等),所以你最终得到了模拟返回嘲笑等等.

  • 示例[here](https://github.com/spring-projects/spring-data-examples/tree/master/jpa/example)使用HSQLDB.切换到H2基本上是在`pom.xml`中交换依赖关系的问题. (7认同)
  • 您是否有链接到内存数据库(例如h2)的集成测试的小示例? (4认同)
  • 谢谢,但我希望看到一个预先填充数据库和/或真正检查数据库的示例. (3认同)

Mar*_*s T 41

使用Spring Boot + Spring Data,它变得非常简单:

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

    @Autowired
    MyRepository subject;

    @Test
    public void myTest() throws Exception {
        subject.save(new MyEntity());
    }
}
Run Code Online (Sandbox Code Playgroud)

@heez的解决方案提出了完整的上下文,这只会带来JPA + Transaction工作所需的内容.请注意,上面的解决方案将显示内存测试数据库,因为可以在类路径中找到一个.

  • @IwoKucharski.你的术语是正确的.但是:鉴于Spring Data为您实现了接口,您很难使用Spring,并且此时它将成为集成测试.如果我问这样的问题,我可能还要求进行单元测试而不考虑术语.因此,我没有将其视为问题的主要或甚至中心点. (12认同)
  • 这是一个**集成**测试,而不是OP提到的**单元**测试 (7认同)
  • `@RunWith(SpringRuner.class)` 现在已经包含在 `@DataJpaTest` 中。 (4认同)

Mil*_*eri 20

这可能有点太晚了,但我已经为此目的写了一些东西.我的库将为您模拟基本的crud存储库方法,并解释查询方法的大多数功能.您必须为自己的本机查询注入功能,但其余的都是为您完成的.

看一看:

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

UPDATE

现在它位于Maven中心,状态非常好.


hee*_*eez 15

如果你正在使用Spring Boot,你可以简单地@SpringBootTest用来加载你的ApplicationContext(这就是你的堆栈跟踪对你的咆哮).这允许您在spring-data存储库中自动装配.请务必添加,@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"));
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以在他们的文档中阅读有关弹簧启动测试的更多信息.

  • 对我来说,存储库始终为空。有什么帮助吗? (2认同)

JRi*_*dsz 11

在 spring boot 2.1.1.RELEASE的最新版本中,它很简单:

@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));
    }
}
Run Code Online (Sandbox Code Playgroud)

完整代码:

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

  • 这是相当不完整的“示例”:无法构建,“集成”测试使用与生产代码相同的配置。IE。没事便是好事。 (3认同)
  • 您应该使用嵌入式数据库进行此测试 (2认同)

Prz*_*wak 5

使用 JUnit5 和@DataJpaTest测试将如下所示(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))
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以使用TestEntityManagerfromorg.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager包来验证实体状态。


Aja*_*mar 5

我用这种方式解决了这个问题 -

    @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;
        .......
        .......
}
Run Code Online (Sandbox Code Playgroud)


Phi*_*rth 5

当您真的想为spring数据存储库编写i-test时,可以这样做:

@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);
    }
}
Run Code Online (Sandbox Code Playgroud)

要遵循此示例,您必须使用以下依赖项:

<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>
Run Code Online (Sandbox Code Playgroud)