我想要在 Spring Data 的帮助下创建一个存储库(例如 UserRepository
)。我是 spring-data 的新手(但不是 spring),我使用这个 tutorial。我选择的处理数据库的技术是 JPA 2.1 和 Hibernate。问题是我对如何为这样的存储库编写单元测试一无所知。
我们以 create()
方法为例。当我首先进行测试时,我应该为它编写一个单元测试——这就是我遇到三个问题的地方:
首先,如何将 EntityManager 的模拟注入到 UserRepository 接口的不存在实现中? Spring Data 将基于此接口生成一个实现: public interface UserRepository extends CrudRepository
其次,我应该测试交互吗?我很难弄清楚应该调用 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
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
等),这样你最终会得到模拟返回模拟等等。
使用 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 工作所需的内容。请注意,鉴于可以在类路径中找到一个内存测试数据库,上述解决方案将调出一个内存测试数据库。
@RunWith(SpringRuner.class)
现在已包含在 @DataJpaTest
中。
@RunWith(SpringRunner.class
启动 spring 上下文,这意味着它正在检查多个单元之间的集成。单元测试正在测试单个单元->单班。然后你写 MyClass sut = new MyClass();
并测试 sut 对象(sut = 被测服务)
这可能来得太晚了,但我已经为此目的写了一些东西。我的库将为您模拟基本的 crud 存储库方法,并解释查询方法的大部分功能。您必须为自己的本机查询注入功能,但其余部分已为您完成。
看一看:
https://github.com/mmnaseri/spring-data-mock
更新
现在它位于 Maven 中心并且状态非常好。
如果您使用的是 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 中进行测试的更多信息。
Predicate
s(这是我的用例)它工作得很好。
当您真的想为 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>
在 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));
}
}
完整代码:
2.0.0.RELEASE
。
使用 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
来验证实体状态。
Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
我通过使用这种方式解决了这个问题 -
@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;
.......
.......
}
您可以使用仅关注 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{
//
}
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.
/ ...
*/
}
}
不定期副业成功案例分享
pom.xml
中的依赖关系的问题。