如何测试 Spring Data 存储库?

我希望在 SpringData 的帮助下创建一个存储库(比如 UserRepository)。我是新的春季数据(但不是春季) ,我使用这个 教程。我选择的数据库处理技术是 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-generated 方法进行单元测试吗?据我所知,第三方库代码不应该是单元测试的——只有开发人员自己编写的代码才应该是单元测试的。但是如果这是真的,它仍然带来了第一个问题回到场景: 比如,我有几个自定义方法用于我的存储库,我将为其编写实现,我如何将我的模拟 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
267401 次浏览

博士

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

细节

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

  • 我们为派生查询创建和缓存 CriteriaQuery实例,以确保查询方法不包含任何输入错误。这需要使用 Criteria API 和 met.model。
  • 我们通过要求 EntityManager为这些查询创建一个 Query实例来验证手动定义的查询(这有效地触发了查询语法验证)。
  • 我们检查 Metamodel是否有关于准备 is-new 检查所处理的域类型的元数据。

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

如果您仔细想想,您不需要为存储库编写任何代码,因此不需要编写任何 单位测试。根本没有必要,因为您可以依赖我们的测试基础来捕获基本的 bug (如果您仍然碰巧遇到一个 bug,请随意提高 罚单)。但是,确实需要集成测试来测试持久层的两个方面,因为它们是与您的领域相关的方面:

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

集成测试

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

测试自定义实现

存储库的自定义实现部分是 以某种方式书写,它们不必知道 Spring Data JPA。它们是普通的春豆,可以注射 EntityManager。当然,你可能想尝试用它来模拟交互,但是老实说,单元测试 JPA 对我们来说并不是一个愉快的经历,因为它可以处理相当多的间接操作(EntityManager-> CriteriaBuilderCriteriaQuery等等) ,所以你最终得到的是模拟返回的模拟等等。

这可能来得有点晚,但我已经写了一些东西,正是为了这个目的。我的库将为您模拟出基本的粗糙存储库方法,并解释您的查询方法的大多数功能。 您将不得不为自己的本机查询注入功能,但其余的工作已经为您完成。

看看吧:

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

更新

现在在 Maven 中心,状况良好。

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

你可以在他们的 医生中阅读更多关于弹簧启动测试的内容。

有了 Spring Boot + Spring Data,它变得非常简单:

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


@Autowired
MyRepository subject;


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

@ hez 的解决方案显示了完整的上下文,这只显示了 JPA + T 事务工作所需的内容。 注意,上面的解决方案将提供一个内存测试数据库,因为可以在类路径中找到这个数据库。

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

我用这种方法解决了这个问题

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

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

在弹簧启动 2.1.1. 释放的最后一个版本中,它简单如下:

@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

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

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 组件@ DataJpaTest注释。默认情况下,它扫描 @ 实体类并配置带有 @ 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());
}
}

Junit4语法将与 SpringRunner 类一起使用。

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