ChatGPT解决这个技术问题 Extra ChatGPT

如何测试 Spring Data 存储库?

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

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

首先,如何将 EntityManager 的模拟注入到 UserRepository 接口的不存在实现中? Spring Data 将基于此接口生成一个实现: public interface UserRepository extends CrudRepository {} 但是,我不知道如何强制它使用 EntityManager 模拟和其他模拟 - 如果我自己编写了实现,我可能会有一个 EntityManager 的 setter 方法,允许我使用我的模拟进行单元测试。 (至于实际的数据库连接,我有一个 JpaConfiguration 类,使用 @Configuration 和 @EnableJpaRepositories 进行注释,它以编程方式为 DataSource、EntityManagerFactory、EntityManager 等定义 bean——但存储库应该是测试友好的并允许覆盖这些东西)。

其次,我应该测试交互吗?我很难弄清楚应该调用 EntityManager 和 Query 的哪些方法(类似于 verify(entityManager).createNamedQuery(anyString()).getResultList();),因为不是我在写实施。

第三,我应该首先对 Spring-Data 生成的方法进行单元测试吗?据我所知,第三方库代码不应该进行单元测试——只有开发人员自己编写的代码才应该进行单元测试。但如果这是真的,它仍然把第一个问题带回了现场:比如说,我有几个自定义方法用于我的存储库,我将为此编写实现,我如何将我的 EntityManager 和 Query 模拟注入到 final ,生成的存储库?

注意:我将使用集成和单元测试来测试我的存储库。对于我的集成测试,我使用的是 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

A
Adriaan Koster

tl;博士

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

细节

我们做了很多前期验证和设置,以确保您只能引导一个没有无效派生查询等的应用程序。

我们为派生查询创建和缓存 CriteriaQuery 实例,以确保查询方法不包含任何拼写错误。这需要使用 Criteria API 以及 meta.model。

我们通过要求 EntityManager 为那些手动定义的查询创建一个 Query 实例(这有效地触发了查询语法验证)来验证手动定义的查询。

我们检查元模型以获取有关处理以准备新检查等的域类型的元数据。

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

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

实体映射

查询语义(无论如何都会在每次引导尝试时验证语法)。

集成测试

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

测试自定义实现

存储库的自定义实现部分是 written in a way,他们不必了解 Spring Data JPA。它们是注入 EntityManager 的普通 Spring bean。您当然可能想尝试模拟与它的交互,但老实说,对 JPA 进行单元测试对我们来说并不是一个太愉快的体验,而且它可以处理很多间接(EntityManager -> { 4}、CriteriaQuery 等),这样你最终会得到模拟返回模拟等等。


您是否有一个使用内存数据库(例如 h2)进行集成测试的小示例的链接?
示例 here 使用 HSQLDB。切换到 H2 基本上是交换 pom.xml 中的依赖关系的问题。
谢谢,但我希望看到一个预先填充数据库和/或真正检查数据库的示例。
“以某种方式编写”背后的链接不再起作用。也许你可以更新它?
那么,您是否也建议对自定义实现使用集成测试而不是单元测试?根本不为他们编写单元测试?只是为了澄清。是的话没关系。我理解原因(太复杂而无法模拟所有事情)。我是 JPA 测试的新手,所以我只想弄清楚。
M
Markus T

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


这是一个集成测试,而不是 OP 提到的单元测试
@IwoKucharski。您对术语是正确的。但是:鉴于 Spring Data 为您实现了接口,您很难使用 Spring,此时它变成了一个集成测试。如果我问这样的问题,我可能还会在不考虑术语的情况下要求进行单元测试。因此,我不认为这是问题的主要,甚至是中心点。
@RunWith(SpringRuner.class) 现在已包含在 @DataJpaTest 中。
@IwoKucharski,为什么这是集成测试,而不是单元测试?
@user1182625 @RunWith(SpringRunner.class 启动 spring 上下文,这意味着它正在检查多个单元之间的集成。单元测试正在测试单个单元->单班。然后你写 MyClass sut = new MyClass(); 并测试 sut 对象(sut = 被测服务)
M
Milad Naseri

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

看一看:

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

更新

现在它位于 Maven 中心并且状态非常好。


h
heez

如果您使用的是 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"));
  }
}

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


这是一个相当好的例子,但在我看来过于简单化了。有没有这种测试甚至会失败的情况?
不是这个本身,但假设您想测试 Predicates(这是我的用例)它工作得很好。
对我来说存储库始终为空。有什么帮助吗?
这是恕我直言最好的答案。通过这种方式,您可以测试创建实体表的 CrudRepo、实体和 DDL 脚本。
我已经写了一个和这个完全一样的测试。当 Repository 的实现使用 jdbcTemplate 时,它可以完美地工作。但是,当我更改 spring-data 的实现(通过从 Repository 扩展接口)时,测试失败并且 userRepository.findOne 返回 null。关于如何解决这个问题的任何想法?
P
Philipp Wirth

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

J
JRichardsz

在 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


这是相当不完整的“示例”:无法构建,“集成”测试使用与生产代码相同的配置。 IE。没事便是好事。
我道歉。因为这个错误,我会鞭打我。请再试一次!
这也适用于 Spring Boot 的 2.0.0.RELEASE
您应该使用嵌入式数据库进行此测试
P
Przemek Nowak

使用 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 来验证实体状态。


为实体 bean 生成 Id 总是更好。
对于 Java,第二行是:@ExtendWith(value = SpringExtension.class)
这给了我一个错误,java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
A
Ajay Kumar

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

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

a
anand krish

您可以使用仅关注 JPA 组件的 @DataJpaTest 注释。默认情况下,它会扫描 @Entity 类并配置带有 @Repository 注释的 Spring Data JPA 存储库。

默认情况下,使用 @DataJpaTest 注释的测试在每个测试结束时为 transactional and roll back

//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{
//
}

R
Rajesh Kakarla
springboot 2.4.5


import javax.persistence.EntityManager;
import javax.persistence.ParameterMode;
import javax.persistence.PersistenceContext;
import javax.persistence.StoredProcedureQuery;

@Repository
public class MyRepositoryImpl implements MyRepository {
    @Autowired
    @PersistenceContext(unitName = "MY_JPA_UNIT")
    private EntityManager entityManager;
    
    
    @Transactional("MY_TRANSACTION_MANAGER")
    @Override
    public MyEntity getSomething(Long id) {
    
        StoredProcedureQuery query = entityManager.createStoredProcedureQuery(
                "MyStoredProcedure", MyEntity.class);
        query.registerStoredProcedureParameter("id", Long.class, ParameterMode.IN);
        query.setParameter("id", id);

        query.execute();
        
        @SuppressWarnings("unchecked")
        MyEntity myEntity = (MyEntity) query.getResultList().stream().findFirst().orElse(null);
        return myEntity;
    }
}

import org.junit.jupiter.api.*;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;

import javax.persistence.EntityManager;
import javax.persistence.StoredProcedureQuery;
import java.util.List;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

@RunWith(MockitoJUnitRunner.Silent.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MyRepositoryTest {

    @InjectMocks
    MyRepositoryImpl myRepository;

    @Mock
    private EntityManager entityManager;

    @Mock
    private StoredProcedureQuery storedProcedureQuery;
    
    @BeforeAll
    public void init() {
        MockitoAnnotations.openMocks(this);
        Mockito.when(entityManager.createStoredProcedureQuery(Mockito.any(), Mockito.any(Class.class)))
                .thenReturn(storedProcedureQuery);
    }

    @AfterAll
    public void tearDown() {
        // something
    }
    
    @Test
    void testMethod() throws Exception {
        Mockito.when(storedProcedureQuery.getResultList()).thenReturn(List.of(myEntityMock));

        MyEntity resultMyEntityList = myRepository.getSomething(1l);

        assertThat(resultMyEntityList,
                allOf(hasProperty("id", org.hamcrest.Matchers.is("1"))
                . . .
        );
    }
}

j
jschnasse

在 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 已经过测试,应该假设是这样。也许我会创建一个单独的测试来检查字段的映射或检查注释中列的值,以检测列值名称的更改。如果您自己启动内存数据库实例并加载表创建脚本
我认为你的评论指向了正确的方向。上面的代码旨在作为 springboot 2.5.1 应用程序上下文的起点。实际的测试代码不是示例的一部分。这个例子只是关于配置。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅