我希望在Spring的帮助下创建一个存储库(比如UserRepository
)。我对spring数据(但不是spring)很陌生,我使用这个教程。我选择的处理数据库的技术是JPA2.1和Hibernate。问题是,对于如何为这样的存储库编写单元测试,我一无所知。
让我们以create()
方法为例。当我在进行测试时--首先,我应该为它编写一个单元测试--这就是我遇到三个问题的地方:
EntityManager
的模拟注入到UserRepository
接口的不存在实现中?Spring数据将根据这个接口生成一个实现:
公共接口UserRepository扩展CrudRepository {}
但是,我不知道如何强制它使用EntityManager
模拟和其他模拟--如果我自己编写了实现,我可能会有一个用于EntityManager
的setter方法,允许我在单元测试中使用模拟。(至于实际的数据库连接,我有一个用@Configuration
和@EnableJpaRepositories
注释的@Configuration
类,它以编程方式为DataSource
、EntityManagerFactory
、EntityManager
等定义beans -但是存储库应该是测试友好的,并且允许覆盖这些东西)。EntityManager
和Query
的哪些方法应该被调用(类似于verify(entityManager).createNamedQuery(anyString()).getResultList();
),因为编写实现的不是我。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
发布于 2014-05-03 01:16:34
tl;dr
简而言之,没有办法合理地对Spring数据JPA存储库进行单元测试,原因很简单:模拟我们调用的用于引导存储库的JPA的所有部分是很麻烦的。无论如何,单元测试在这里没有多大意义,因为您通常不会自己编写任何实现代码(参见下面关于自定义实现的段落),因此集成测试是最合理的方法。
详细信息
我们做了相当多的前期验证和设置,以确保您只能引导一个应用程序,没有无效的派生查询等。
CriteriaQuery
实例,以确保查询方法不包含任何类型的输入。这需要使用标准API以及meta.model。EntityManager
为这些查询创建一个Query
实例来验证手动定义的查询(这有效地触发了查询语法验证)。Metamodel
中关于处理域类型的元数据,准备is-新检查等等。您可能会在手写存储库中延迟的所有内容,这可能会导致应用程序在运行时中断(因为无效的查询等等)。
考虑一下,您没有为存储库编写代码,因此没有必要编写任何_unit_tests。根本没有必要,因为您可以依靠我们的测试库来捕获基本的bug(如果您仍然遇到一个bug,可以随意地引发一个票证)。但是,肯定需要集成测试来测试持久化层的两个方面,因为它们是与您的域相关的方面:
集成测试
这通常是通过使用内存中的数据库和测试用例来实现的,这些测试用例通常通过测试上下文框架引导Spring ApplicationContext
(就像您已经做的那样),预先填充数据库(通过EntityManager
或repo插入对象实例,或者通过普通的SQL文件),然后执行查询方法来验证它们的结果。
测试自定义实现
存储库的自定义实现部分是以某种方式写成,它们不需要了解Spring。它们是普通的Spring,会被注入EntityManager
。当然,你可能想试着嘲笑它的交互,但老实说,对我们来说,对JPA进行单元测试并不是一种令人愉快的体验,而且它与相当多的间接(EntityManager
,->,CriteriaBuilder
,CriteriaQuery
等)一起工作。这样,您就可以得到模拟、返回的模拟等等。
发布于 2017-04-25 04:28:58
对于Spring + Spring数据,它变得非常容易:
@RunWith(SpringRunner.class)
@DataJpaTest
public class MyRepositoryTest {
@Autowired
MyRepository subject;
@Test
public void myTest() throws Exception {
subject.save(new MyEntity());
}
}
@heez的解决方案提供了完整的上下文,这只会带来JPA+Transaction工作所需的内容。注意,上面的解决方案将弹出内存测试数据库,因为可以在类路径上找到一个数据库。
发布于 2015-02-20 22:46:14
这可能来得太晚了,但我已经为此目的写了一些东西。我的库将为您模拟基本的crud存储库方法,并解释查询方法的大部分功能。您必须为您自己的本机查询注入功能,但其余的功能都是为您完成的。
看一看:
https://github.com/mmnaseri/spring-data-mock
更新
这是现在的Maven中心和相当好的状态。
https://stackoverflow.com/questions/23435937
复制