我想要一个存储库(比如说,UserRepository
)在 Spring Data 的帮助下创建。我是 spring-data 的新手(但不是 spring),我使用这个tutorial。我选择的处理数据库的技术是 JPA 2.1 和 Hibernate。问题是我对如何为这样的存储库编写单元测试一无所知。
让我们来create()
方法为例。由于我正在以测试为先,因此我应该为其编写一个单元测试 - 这就是我遇到三个问题的地方:
-
首先,我如何注入一个模拟EntityManager
进入不存在的实现UserRepository
界面? Spring Data 将生成基于此接口的实现:
public interface UserRepository extends CrudRepository<User, Long> {}
但是,我不知道如何强制它使用EntityManager
模拟和其他模拟 - 如果我自己编写实现,我可能会有一个 setter 方法EntityManager
,允许我使用我的模拟进行单元测试。 (至于实际的数据库连接,我有一个JpaConfiguration
类,注释为@Configuration
and @EnableJpaRepositories
,它以编程方式定义 beanDataSource
, EntityManagerFactory
, EntityManager
等等 - 但存储库应该是测试友好的,并允许覆盖这些东西)。
其次,我应该测试交互吗?我很难弄清楚有哪些方法EntityManager
and Query
应该被称为(类似于verify(entityManager).createNamedQuery(anyString()).getResultList();
),因为不是我在编写实现。
第三,我是否应该首先对 Spring-Data 生成的方法进行单元测试?据我所知,第三方库代码不应该进行单元测试 - 只有开发人员自己编写的代码才应该进行单元测试。但如果这是真的,它仍然把第一个问题带回到现场:比如说,我的存储库有几个自定义方法,我将为其编写实现,我如何注入我的模拟EntityManager
and Query
进入最终生成的存储库?
注意:我将使用以下方式测试我的存储库both集成和单元测试。对于我的集成测试,我使用 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
tl;dr
简而言之,没有办法对 Spring Data JPA 存储库进行合理的单元测试,原因很简单:模拟我们调用来引导存储库的 JPA API 的所有部分是很麻烦的。无论如何,单元测试在这里没有太大意义,因为您通常不会自己编写任何实现代码(请参阅下面有关自定义实现的段落),因此集成测试是最合理的方法。
Details
我们进行了大量的前期验证和设置,以确保您只能引导没有无效派生查询等的应用程序。
- 我们创建并缓存
CriteriaQuery
派生查询的实例,以确保查询方法不包含任何拼写错误。这需要使用 Criteria API 以及 meta.model。
- 我们通过询问来验证手动定义的查询
EntityManager
创建一个Query
这些实例(有效地触发查询语法验证)。
- 我们检查
Metamodel
用于准备 is-new 检查等的有关域类型的元数据。
您可能会推迟在手写存储库中的所有内容,这可能会导致应用程序在运行时中断(由于无效查询等)。
如果您考虑一下,您无需为存储库编写任何代码,因此无需编写任何代码unit测试。根本没有必要,因为您可以依靠我们的测试库来捕获基本错误(如果您仍然碰巧遇到错误,请随时提出ticket)。但是,肯定需要集成测试来测试持久层的两个方面,因为它们是与您的域相关的方面:
- 实体映射
- 查询语义(无论如何,每次引导尝试都会验证语法)。
集成测试
这通常是通过使用内存数据库和引导 Spring 的测试用例来完成的ApplicationContext
通常通过测试上下文框架(正如您已经做的那样),预填充数据库(通过通过插入对象实例EntityManager
或 repo,或通过纯 SQL 文件),然后执行查询方法来验证它们的结果。
测试自定义实现
存储库的自定义实现部分是以某种方式写的他们不必了解 Spring Data JPA。它们是普通的春豆,EntityManager
注射。您当然可能想尝试模拟与它的交互,但说实话,对 JPA 进行单元测试对我们来说并不是一次太愉快的经历,而且它可以处理很多间接操作(EntityManager
-> CriteriaBuilder
, CriteriaQuery
等),这样你最终会得到模拟返回模拟等等。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)