如何测试Spring Data存储库?

189
我想创建一个使用Spring Data帮助的存储库(例如,UserRepository)。我对spring-data是新手(但不是对spring),我使用这个教程。我选择用JPA 2.1和Hibernate处理数据库技术。问题在于,我不知道如何为这样的存储库编写单元测试。
以create()方法为例。因为我是先编写测试再编写代码,所以我应该为它编写一个单元测试-这就是我遇到的三个问题:
首先,我应该如何将EntityManager的模拟注入到UserRepository接口的不存在的实现中?Spring Data会根据这个接口生成一个实现:
public interface UserRepository extends CrudRepository<User, Long> {}

然而,我不知道如何强制它使用EntityManager模拟和其他模拟 - 如果是我自己编写实现,我可能会为EntityManager编写一个setter方法,使我可以在单元测试中使用我的模拟。 (至于实际的数据库连接,我有一个JpaConfiguration类,带有@Configuration@EnableJpaRepositories注释,以编程方式为DataSourceEntityManagerFactoryEntityManager等定义bean——但是库应该是易于测试的,并允许覆盖这些内容)。

第二个问题,我是否应该测试交互?我很难弄清楚应该调用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());
}

}

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

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
11个回答

161

简短概述

简而言之,对于Spring Data JPA存储库,没有合理的单元测试方法,因为模拟我们调用以引导存储库的JPA API的所有部分太麻烦了。无论如何,在这里编写单元测试并没有太多意义(有关自定义实现的段落请参见下文),因此集成测试是最合理的方法。

详细信息

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

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

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

如果您仔细思考一下,就会发现您不需要编写存储库的任何代码,因此也不需要编写任何单元测试。没有这个必要,因为您可以依赖于我们的测试基础来捕获基本错误(如果您仍然遇到问题,请随时提出工单)。但是,确实需要集成测试来测试持久性层的两个方面,因为它们与您的领域相关:

  • 实体映射
  • 查询语义(语法在每次引导尝试中都得到验证)。

集成测试

通常使用内存数据库和测试用例来完成这个过程,通过测试上下文框架(如您已经做的那样),引导一个Spring ApplicationContext ,向数据库预填充数据(通过EntityManager或repo插入对象实例,或通过纯SQL文件进行)然后执行查询方法以验证其结果。

测试自定义实现

仓库的自定义实现部分编写方式,不必知道Spring Data JPA。它们是普通的Spring bean,注入了一个EntityManager。当然,您可能想尝试模拟与它的交互,但说实话,对我们来说单元测试JPA并不是一种太愉快的体验,因为它涉及到相当多的间接性(EntityManager -> CriteriaBuilderCriteriaQuery等),因此你最终得到的模拟返回模拟等等。


7
你有没有一个小的整合测试示例链接,使用内存数据库(例如h2)? - Wim Deblauwe
7
这里的示例代码使用的是HSQLDB数据库。如果要切换到H2数据库,只需在pom.xml中更改依赖即可。 - Oliver Drotbohm
3
谢谢,但我希望看到一个预填充数据库和/或真正检查数据库的示例。 - Wim Deblauwe
1
"written in a way" 后面的链接已经失效了,也许您可以更新一下? - Wim Deblauwe
1
所以,您建议对于自定义实现使用集成测试而不是单元测试?并且根本不为它们编写单元测试?只是为了澄清。如果是的话那没问题。我理解原因(模拟所有事情太复杂了)。我对JPA测试还很陌生,所以我只是想弄清楚。 - Ruslan Stelmachenko
1
@djxak - 一般来说:是的。 - Oliver Drotbohm

69

使用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工作所需的内容。 请注意,上面的解决方案将带来一个内存中的测试数据库,前提是可以在类路径上找到。


22
这是一项集成测试,而不是OP提到的单元测试 - Iwo Kucharski
24
@IwoKucharski,您对术语的理解是正确的。但是:鉴于Spring Data为您实现了接口,您很难不使用Spring,此时就变成了一个集成测试。如果我像这样提出了一个问题,我可能也没有考虑术语的区别,因此我并未将其视为问题的主要或甚至核心点。 - Markus T
7
现在@DataJpaTest中已经包含了@RunWith(SpringRunner.class) - Maroun
@IwoKucharski,为什么这是集成测试而不是单元测试? - user1182625
@user1182625 @RunWith(SpringRunner.class) 启动 Spring 上下文,这意味着它正在检查多个单元之间的集成。单元测试是测试单个单元 -> 单个类。然后您编写 MyClass sut = new MyClass(); 并测试 sut 对象(sut = 测试服务)。 - Iwo Kucharski
1
@DataJpaTest使用时,测试是否实际保存到数据库?如果是的话,那么在测试中是否应该删除插入的行?只是想知道当单元(或集成)测试实际插入到数据库时,整体测试方法会是什么样子。 - Woodchuck

28

可能有点晚了,但我已经为此编写了一些内容。我的库将为您模拟基本的CRUD存储库方法,并解释大多数查询方法的功能。

您需要为自己的本地查询注入功能,但其余部分都已经完成了。

看一下:

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

更新

现在这个库已经在Maven中央仓库中,而且状态非常不错。


26

如果你正在使用Spring Boot,你可以简单地使用@SpringBootTest来加载你的ApplicationContext(这就是你的堆栈跟踪在抱怨的内容)。这允许你自动装配你的Spring Data仓库。确保添加@RunWith(SpringRunner.class)以便捕捉到Spring特定的注释:

@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
不是这个特定的例子,但是假设你想要测试“Predicate”(这是我的用例),它非常有效。 - heez
2
我的存储库始终为空。有什么帮助吗? - Atul Chaudhary
这是我个人认为最好的答案。这种方法可以测试CrudRepo、Entity和DDL脚本,这些脚本创建实体表格。 - MirandaVeracruzDeLaHoyaCardina
我已经写了一个和这个测试完全一样的测试。当Repository的实现使用jdbcTemplate时,它可以完美地工作。然而,当我改变实现为spring-data(通过扩展Repository接口)时,测试失败了,userRepository.findOne返回null。有什么解决方法吗? - Rega

13
在最新版本的Spring Boot中2.1.1.RELEASE,它很简单:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SampleApplication.class)
public class CustomerRepositoryTest {

    @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
1
这也适用于Spring Boot的2.0.0.RELEASE版本。 - Nital
2
你应该在这个测试中使用嵌入式数据库。 - TuGordoBello
我不明白这些测试如何节省时间,也不知道它们测试什么?如果你的 data.sql 文件注入了测试数据,并且在服务器运行时无法更改数据,那还有什么意义呢? - Ray Bond

12

当你真的想为Spring Data Repository编写一个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;
        .......
        .......
}

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

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

最好使用Spring为实体Bean生成ID。 - Arundev
1
对于Java,第二行是:@ExtendWith(value = SpringExtension.class)。 - Oozeerally
1
这给了我一个错误,java.lang.IllegalStateException: 无法找到@SpringBootConfiguration,您需要在测试中使用@ContextConfiguration或@SpringBootTest(classes=...) - Dilshan Harshana

2
你可以使用@DataJpaTest注解,该注解只关注JPA组件。默认情况下,它会扫描@Entity类,并配置使用@Repository注解的Spring Data JPA存储库。
默认情况下,使用@DataJpaTest注解的测试是事务性的并且在每个测试结束时回滚
//in Junit 5 @RunWith(SpringRunner.class) annotation is not required

@DataJpaTest
public class EmployeeRepoTest {

@Autowired
EmployeeRepo repository;
 
@Test
public void testRepository() 
{
    EmployeeEntity employee = new EmployeeEntity();
    employee.setFirstName("Anand");
    employee.setProject("Max Account");
     
    repository.save(employee);
     
    Assert.assertNotNull(employee.getId());
  }
}

Junit 4语法将与SpringRunner类一起使用。

//Junit 4
@RunWith(SpringRunner.class)
@DataJpaTest
public class DataRepositoryTest{
//
}

1
在2021年,使用新初始化的springboot 2.5.1项目,我正在这样做:
...
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;


@ExtendWith(MockitoExtension.class)
@DataJpaTest
public class SomeTest {

    @Autowired
    MyRepository repo;

    @Test
    public void myTest() throws Exception {
        repo.save(new MyRepoEntity());
        /*...
        / Actual Test. For Example: Will my queries work? ... etc.
        / ...
        */
    }
}

我认为这是毫无意义的测试。你正在测试JPA而不是你的代码。JPA已经经过测试,应该被视为已知。也许我会创建一个单独的测试来检查字段映射或检查注释中列的值,以便检测列值名称的更改。这是在您自己启动内存数据库实例并加载表创建脚本的情况下。 - Ciccio
我认为你的评论指向了正确的方向。上面的代码旨在作为SpringBoot 2.5.1应用程序上下文中的起点工作。实际的测试代码不是示例的一部分,示例只涉及配置。 - jschnasse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接