如何测试Spring Data存储库?


136

我想要UserRepository借助Spring Data创建的存储库(例如)。我是spring-data的新手(但不是spring),并且使用了本教程。我选择的用于处理数据库的技术是JPA 2.1和Hibernate。问题是我对如何为这样的存储库编写单元测试一无所知。

让我们以create()方法为例。当我首先进行测试时,应该为它编写一个单元测试-这就是我遇到的三个问题:

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

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

    但是,我不知道如何强制它使用EntityManager模拟和其他模拟-如果我自己编写实现,则可能会有一个setter方法EntityManager,允许我将模拟用于单元测试。(对于实际的数据库连接性,我有一个JpaConfiguration类,用@Configuration和注释,该类以@EnableJpaRepositories编程方式定义了,等等的Bean DataSource,但是存储库应该易于测试,并且可以覆盖这些内容)。EntityManagerFactoryEntityManager

  • 其次,我应该测试互动吗?我很难弄清楚应该调用EntityManager和的什么方法Query(类似于verify(entityManager).createNamedQuery(anyString()).getResultList();),因为编写实现的不是我。

  • 第三,首先我应该对Spring-Data生成的方法进行单元测试吗?据我所知,第三方库代码不应该进行单元测试-只有开发人员自己编写的代码才应该进行单元测试。但是,如果这是真的,但它仍然带来的第一个问题回到场景:比方说,我有一对夫妇的自定义方法为我的仓库,为此,我会写的实现,我怎么注入我的嘲笑EntityManager,并Query进入决赛,产生仓库?

注:我将使用测试驱动我的仓库集成和单元测试。对于我的集成测试,我使用的是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());
}

}

但是,运行此测试将为我提供以下堆栈跟踪:

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

Answers:


118

tl; dr

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

细节

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

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

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

如果您考虑一下,则无需为存储库编写任何代码,因此无需编写任何单元测试。有根本没有必要,你可以依靠我们的试验基地搭上基本的错误(如果你还凑巧碰上一个,随便养)。但是,绝对需要进行集成测试来测试持久层的两个方面,因为它们是与您的域相关的方面:

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

整合测试

这通常是通过使用内存数据库和ApplicationContext通常通过测试上下文框架(如您已经做过)引导Spring 的测试用例(已经完成),预填充数据库(通过在EntityManageror或repo中插入对象实例或通过简单地插入)来完成的。SQL文件),然后执行查询方法以验证其结果。

测试自定义实现

存储库的自定义实现部分以不必了解Spring Data JPA 的方式编写。它们是普通的四季豆,可以EntityManager注射。当然,你可能会想尝试嘲笑的相互作用与它,但说实话,单元测试的JPA一直不是我们太愉快的经历,以及它的工作原理与相当多的间接性(EntityManager- > CriteriaBuilderCriteriaQuery等等),所以最终导致模拟返回模拟,依此类推。


5
您是否有指向内存数据库(例如h2)集成测试的小示例的链接?
Wim Deblauwe,2015年

7
此处的示例使用HSQLDB。切换到H2基本上是交换pom.xml
奥利弗·德罗博姆

3
谢谢,但是我希望看到一个可以预填充数据库和/或真正检查数据库的示例。
Wim Deblauwe,2015年

1
“以某种方式编写”后面的链接不再起作用。也许您可以更新它?
Wim Deblauwe,2015年

1
因此,您建议也将集成测试而不是单元测试用于自定义实现吗?根本不为他们编写单元测试吗?只是为了澄清。可以,可以。我了解原因(太复杂了,无法模拟所有事物)。我是JPA测试的新手,所以我只想弄清楚。
Ruslan Stelmachenko

48

使用Spring Boot + Spring Data变得非常容易:

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

    @Autowired
    MyRepository subject;

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

@heez的解决方案提出了完整的上下文,这仅提出了JPA + Transaction正常工作所需的内容。请注意,鉴于可以在类路径上找到一个数据库,上述解决方案将打开一个内存测试数据库。


7
这是OP提到的集成测试,而不是单元测试
Iwo Kucharski '18年

16
@IwoKucharski。您对术语是正确的。但是:鉴于Spring Data为您实现了接口,因此您很难使用Spring,这时它将成为集成测试。如果我问这样的问题,我可能还要求进行单元测试,而无需考虑术语。因此,我认为这不是问题的重点,甚至不是重点。
Markus T

@RunWith(SpringRuner.class)现在已包含在中@DataJpaTest
Maroun

@IwoKucharski,为什么这是集成测试,而不是单元测试?
user1182625

@ user1182625 @RunWith(SpringRunner.class启动spring上下文,这意味着它正在检查多个单元之间的集成。单元测试是测试单个单元->单个类。然后,您编写MyClass sut = new MyClass();并测试sut对象(sut =测试中的服务)
Iwo Kucharski


16

如果您使用的是Spring Boot,则可以简单地使用@SpringBootTest来加载ApplicationContext(这是您的stacktrace对您的咆哮)。这使您可以自动连接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"));
  }
}

您可以在他们的文档中阅读有关Spring Boot测试的更多信息。


这是一个很好的例子,但我认为这很简单。是否有任何情况下该测试甚至可能失败?
HopeKing

本身不是这个,但是假设您想测试Predicates(这是我的用例),它工作得很好。
heez

1
对我而言,存储库始终为null。有什么帮助吗?
Atul Chaudhary

这是恕我直言的最佳答案。这样,您将测试CrudRepo,实体和创建实体表的DDL脚本。
MirandaVeracruzDeLaHoyaCardina

我已经编写了与此完全相同的测试。当存储库的实现使用jdbcTemplate时,它可以完美地工作。但是,当我更改spring-data的实现(通过从Repository扩展接口)时,测试失败,并且userRepository.findOne返回null。关于如何解决这个问题的任何想法?
雷加

8

在最新版本的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));
    }
}

完整的代码:

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


3
这是相当不完整的“示例”:无法构建,“集成”测试使用与生产代码相同的配置。就是 没事便是好事。
Martin Mucha

我道歉。因为这个错误,我会鞭打我。请再试一次!
JRichardsz

这也适用于2.0.0.RELEASESpring Boot。
Nital

您应该在此测试中使用嵌入式数据库
TuGordoBello

7

当您真的想为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);
    }
}

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

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

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;
        .......
        .......
}

4

使用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))
    }
}

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


为实体bean生成ID总是更好的选择。
阿伦杰夫

对于Java,第二行是:@ExtendWith(value = SpringExtension.class)
AdilOoze
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.