在测试Java EE应用程序时,我们可以使用各种工具和方法。根据给定测试的具体目标和要求,选项范围从单个类的普通单元测试到部署到容器中的综合集成测试(例如通过 Arquillian ),并通过 REST Assured 等工具驱动。
在这篇文章中,我想讨论一种代表某种中间立场的测试方法:启动本地CDI容器和连接到内存数据库的JPA运行时。这样,您就可以在纯Java SE下测试CDI bean(例如包含业务逻辑)和持久层(例如,基于JPA的存储库)。
这允许在与其他人交互时测试各个类和组件(例如,在测试业务逻辑时不需要模拟存储库),同时仍然受益于快速执行时间(不需要容器管理/部署和远程API调用)。该方法还允许测试我们的应用程序可能依赖的服务,例如拦截器,事件,事务语义和其他需要部署到容器中的东西。最后,这些测试很容易调试,因为一切都在本地VM中运行,并且不涉及远程进程。
为了使该方法有价值,测试基础设施应该启用以下内容:
在下面我们看看如何解决这些要求。您可以在GitHub上的Hibernate 示例存储库 中找到所显示代码的完整版本。该示例项目使用 Weld 作为CDI容器, Hibernate ORM 作为JPA提供程序, H2 作为数据库。请注意,帖子主要关注CDI和持久层的交互,您也可以将此方法用于任何其他数据库,如Postgres或MySQL。
通过依赖注入获取CDI Bean
使用CDI 2.0中标准化的 bootstrap API 在Java SE下启动CDI容器是简单的。所以我们可以在测试中简单地使用该API。另一个需要考虑的方法是 Weld JUnit ,这是Weld(CDI参考实现)的一个小扩展,旨在用于测试目的。除此之外,Weld JUnit允许将依赖项注入测试类并在测试期间启用特定的CDI范围。@RequestScoped例如,在测试bean 时这会派上用场。
使用Weld JUnit的第一个简单测试可能如下所示(注意我在这里使用JUnit 4 API,但是Weld JUnit也 支持JUnit 5 ):
<b>public</b> <b>class</b> SimpleCdiTest { @Rule <b>public</b> WeldInitiator weld = WeldInitiator.from(GreetingService.<b>class</b>) .activate(RequestScoped.<b>class</b>) .inject(<b>this</b>) .build(); @Inject <b>private</b> GreetingService greeter; @Test <b>public</b> <b>void</b> helloWorld() { assertThat(greeter.greet(<font>"Java"</font><font>)).isEqualTo(</font><font>"Hello, Java"</font><font>); } } </font>
通过依赖注入获取JPA实体管理器
在下一步中,让我们看看如何通过依赖注入获取JPA实体管理器。通常你会使用@PersistenceContext注释获得这样的引用(实际上Weld JUnit提供了一种启用它的方法),但为了与其他注入点保持一致,我更喜欢通过 JSR 330 定义的@Inject获取实体管理器。这也允许构造函数注入而不是字段注入。
为此,我们可以简单地定义一个CDI生成器EntityManagerFactory:
@ApplicationScoped <b>public</b> <b>class</b> EntityManagerFactoryProducer { @Produces @ApplicationScoped <b>public</b> EntityManagerFactory produceEntityManagerFactory() { <b>return</b> Persistence.createEntityManagerFactory(<font>"myPu"</font><font>, <b>new</b> HashMap<>()); } <b>public</b> <b>void</b> close(@Disposes EntityManagerFactory entityManagerFactory) { entityManagerFactory.close(); } } </font>
这使用JPA引导程序API来构建(应用程序作用域)实体管理器工厂。以类似的方式,可以生成请求范围的实体管理器bean:
@ApplicationScoped <b>public</b> <b>class</b> EntityManagerProducer { @Inject <b>private</b> EntityManagerFactory entityManagerFactory; @Produces @RequestScoped <b>public</b> EntityManager produceEntityManager() { <b>return</b> entityManagerFactory.createEntityManager(); } <b>public</b> <b>void</b> close(@Disposes EntityManager entityManager) { entityManager.close(); } }
请注意,如果您的主代码中已经有这样的生成器,则必须将这些bean注册为 备选方案 。
有了生产者,我们可以通过@Inject以下方式将实体经理注入CDI bean :
@ApplicationScoped <b>public</b> <b>class</b> GreetingService { <b>private</b> <b>final</b> EntityManager entityManager; @Inject <b>public</b> GreetingService(EntityManager entityManager) { <b>this</b>.entityManager = entityManager; } <font><i>// ...</i></font><font> } </font>
JPA实体监听器中的依赖注入
JPA 2.1在JPA实体监听器中引入了对CDI的支持。为此,JPA提供程序(例如Hibernate ORM)必须具有对当前CDI bean管理器的引用。
在像 WildFly 这样的应用程序服务器中,容器会自动为我们连接。对于我们的测试设置,我们需要在引导JPA时自己传递bean管理器引用。幸运的是,这不是太复杂; 在EntityManagerFactoryProducer类中,我们可以通过@Inject获取BeanManager实例,然后使用“javax.persistence.bean.manager”属性键将其传递给JPA:
@Inject <b>private</b> BeanManager beanManager; @Produces @ApplicationScoped <b>public</b> EntityManagerFactory produceEntityManagerFactory() { Map<String, Object> props = <b>new</b> HashMap<>(); props.put(<font>"javax.persistence.bean.manager"</font><font>, beanManager); <b>return</b> Persistence.createEntityManagerFactory(</font><font>"myPu"</font><font>, props); } </font>
这让我们可以在JPA实体监听器中使用依赖注入:
@ApplicationScoped <b>public</b> <b>class</b> SomeListener { <b>private</b> <b>final</b> GreetingService greetingService; @Inject <b>public</b> SomeListener(GreetingService greetingService) { <b>this</b>.greetingService = greetingService; } @PostPersist <b>public</b> <b>void</b> onPostPersist(TestEntity entity) { greetingService.greet(entity.getName()); } }
声明式事务控制via @Transactional和事务性事件观察器
满足我们原始要求的最后一个缺失部分是对@Transactional注释和事务事件观察者的支持。这个要复杂得多,因为它需要集成与JTA兼容的事务管理器(Java Transaction API)。
在下文中,我们将使用 Narayana ,它也是WildFly中使用的事务管理器。要使Narayana工作,需要一个JNDI服务器,它可以从中获取JTA数据源。此外,还需要焊接JTA模块。请参阅示例项目的 pom.xml 以获取确切的工件ID和版本。
有了这些依赖关系,下一步就是将自定义ConnectionProvider插入Hibernate ORM,这可以确保Hibernate ORM与Connection使用Narayana管理的事务的对象一起工作。值得庆幸的是,我的同事Gytis Trikleris已经提供了 这样的实现, 作为GitHub上Narayana示例的一部分。我无耻地要复制这个实现:
<b>public</b> <b>class</b> TransactionalConnectionProvider implements ConnectionProvider { <b>public</b> <b>static</b> <b>final</b> String DATASOURCE_JNDI = <font>"java:testDS"</font><font>; <b>public</b> <b>static</b> <b>final</b> String USERNAME = </font><font>"sa"</font><font>; <b>public</b> <b>static</b> <b>final</b> String PASSWORD = </font><font>""</font><font>; <b>private</b> <b>final</b> TransactionalDriver transactionalDriver; <b>public</b> TransactionalConnectionProvider() { transactionalDriver = <b>new</b> TransactionalDriver(); } <b>public</b> <b>static</b> <b>void</b> bindDataSource() { JdbcDataSource dataSource = <b>new</b> JdbcDataSource(); dataSource.setURL(</font><font>"jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1"</font><font>); dataSource.setUser(USERNAME); dataSource.setPassword(PASSWORD); <b>try</b> { InitialContext initialContext = <b>new</b> InitialContext(); initialContext.bind(DATASOURCE_JNDI, dataSource); } <b>catch</b> (NamingException e) { <b>throw</b> <b>new</b> RuntimeException(e); } } @Override <b>public</b> Connection getConnection() throws SQLException { Properties properties = <b>new</b> Properties(); properties.setProperty(TransactionalDriver.userName, USERNAME); properties.setProperty(TransactionalDriver.password, PASSWORD); <b>return</b> transactionalDriver.connect(</font><font>"jdbc:arjuna:"</font><font> + DATASOURCE_JNDI, properties); } @Override <b>public</b> <b>void</b> closeConnection(Connection connection) throws SQLException { <b>if</b> (!connection.isClosed()) { connection.close(); } } @Override <b>public</b> <b>boolean</b> supportsAggressiveRelease() { <b>return</b> false; } @Override <b>public</b> <b>boolean</b> isUnwrappableAs(Class aClass) { <b>return</b> getClass().isAssignableFrom(aClass); } @Override <b>public</b> <T> T unwrap(Class<T> aClass) { <b>if</b> (isUnwrappableAs(aClass)) { <b>return</b> (T) <b>this</b>; } <b>throw</b> <b>new</b> UnknownUnwrapTypeException(aClass); } } </font>
这将注册一个带有JNDI的H2数据源,TransactionalDriver当Hibernate ORM请求连接时,Narayana 会从中获取它。此连接将使用JTA事务,无论事务是@Transactional通过注入UserTransaction还是使用实体管理器事务API 以声明方式(通过)进行控制。
bindDataSource()必须在测试执行之前调用该方法。将该步骤封装在自定义 JUnit规则 中是个好主意,这样可以在不同的测试中轻松地重用此设置:
<b>public</b> <b>class</b> JtaEnvironment <b>extends</b> ExternalResource { <b>private</b> NamingBeanImpl NAMING_BEAN; @Override <b>protected</b> <b>void</b> before() throws Throwable { NAMING_BEAN = <b>new</b> NamingBeanImpl(); NAMING_BEAN.start(); JNDIManager.bindJTAImplementation(); TransactionalConnectionProvider.bindDataSource(); } @Override <b>protected</b> <b>void</b> after() { NAMING_BEAN.stop(); } }
这将启动JNDI服务器并将事务管理器以及数据源绑定到JNDI树。在实际测试类中,我们需要做的就是创建该规则的实例并使用如以下内容@Rule注释该字段:
<b>public</b> <b>class</b> CdiJpaTest { @ClassRule <b>public</b> <b>static</b> JtaEnvironment jtaEnvironment = <b>new</b> JtaEnvironment(); @Rule <b>public</b> WeldInitiator weld = ...; @Test <b>public</b> <b>void</b> someTest() { <font><i>// ...</i></font><font> } } </font>
在下一步中,必须使用Hibernate ORM注册连接提供程序。这可以在persistence.xml中完成,但由于此提供程序只应在测试期间使用,因此更好的地方是我们的实体管理器工厂生产者方法:
@Produces @ApplicationScoped <b>public</b> EntityManagerFactory produceEntityManagerFactory() { Map<String, Object> props = <b>new</b> HashMap<>(); props.put(<font>"javax.persistence.bean.manager"</font><font>, beanManager); props.put(Environment.CONNECTION_PROVIDER, TransactionalConnectionProvider.<b>class</b>); <b>return</b> Persistence.createEntityManagerFactory(</font><font>"myPu"</font><font>, props); } </font>
为了将Weld与事务管理器连接起来,需要实现Weld的 TransactionServices SPI:
<b>public</b> <b>class</b> TestingTransactionServices implements TransactionServices { @Override <b>public</b> <b>void</b> cleanup() { } @Override <b>public</b> <b>void</b> registerSynchronization(Synchronization synchronizedObserver) { jtaPropertyManager.getJTAEnvironmentBean() .getTransactionSynchronizationRegistry() .registerInterposedSynchronization(synchronizedObserver); } @Override <b>public</b> <b>boolean</b> isTransactionActive() { <b>try</b> { <b>return</b> com.arjuna.ats.jta.UserTransaction.userTransaction().getStatus() == Status.STATUS_ACTIVE; } <b>catch</b> (SystemException e) { <b>throw</b> <b>new</b> RuntimeException(e); } } @Override <b>public</b> UserTransaction getUserTransaction() { <b>return</b> com.arjuna.ats.jta.UserTransaction.userTransaction(); } }
这让Weld
该TransactionServices实施拿起使用的服务加载机制,使文件META-INF /服务/ org.jboss.weld.bootstrap.api.Service需要与我们的执行情况及其内容的完全限定名称:
org.hibernate.demos.jpacditesting.support.TestingTransactionServices
有了它,我们现在可以测试使用事务观察器的代码:
@ApplicationScoped <b>public</b> <b>class</b> SomeObserver { <b>public</b> <b>void</b> observes(@Observes(during=TransactionPhase.AFTER_COMPLETION) String event) { <font><i>// handle event ...</i></font><font> } } </font>
我们还可以使用JTA的@Transactional注释从声明式事务控制中受益:
@ApplicationScoped <b>public</b> <b>class</b> TransactionalGreetingService { @Transactional(TxType.REQUIRED) <b>public</b> String greet(String name) { <font><i>// ...</i></font><font> } } </font>
greet()调用此方法时,它必须在事务上下文中运行,该事务上下文已在之前启动或在需要时启动。现在,如果您之前使用过事务CDI bean,您可能想知道关联的方法拦截器在哪里。事实证明, Narayana 自带CDI支持和为我们提供了所需要的一切:为不同的事务行为方法的拦截器(REQUIRED,MANDATORY等),以及作为与CDI容器注册拦截器的便携式扩展。
配置Weld 启动器
到目前为止,我们已经忽略了最后一个细节,这就是Weld将如何检测我们测试所需的所有bean,无论是测试中的实际组件GreetingService,还是测试基础设施,如EntityManagerProducer。最简单的方法是让Weld扫描类路径本身并获取它找到的所有bean。通过将新Weld实例传递给WeldInitiator规则来启用此功能:
<b>public</b> <b>class</b> CdiJpaTest { @ClassRule <b>public</b> <b>static</b> JtaEnvironment jtaEnvironment = <b>new</b> JtaEnvironment(); @Rule <b>public</b> WeldInitiator weld = WeldInitiator.from(<b>new</b> Weld()) .activate(RequestScoped.<b>class</b>) .inject(<b>this</b>) .build(); @Inject <b>private</b> EntityManager entityManager; @Inject <b>private</b> GreetingService greetingService; @Test <b>public</b> <b>void</b> someTest() { <font><i>// ...</i></font><font> } } </font>
这非常方便,但它可能会导致较大的类路径有些缓慢,例如暴露您不希望为特定测试启用的替代bean。因此,可以显式传递在测试期间使用的所有bean类型:
@Rule <b>public</b> WeldInitiator weld = WeldInitiator.from( GreetingService.<b>class</b>, TransactionalGreetingService.<b>class</b>, EntityManagerProducer.<b>class</b>, EntityManagerFactoryProducer.<b>class</b>, TransactionExtension.<b>class</b>, <font><i>// ...</i></font><font> ) .activate(RequestScoped.<b>class</b>) .inject(<b>this</b>) .build(); </font>
这避免了类路径扫描,但代价是增加了编写和维护测试的工作量。另一种方法是使用该Weld#addPackages()方法并指定要包括在包的粒度中的内容。我的建议是采用类路径扫描方法,如果扫描实际上不可行,则只切换到显式列出所有类。
总结
在这篇文章中,我们探讨了如何在普通Java SE环境中结合基于JPA的持久层测试应用程序的CDI bean。对于某些测试而言,这可能是一个有趣的中间点,您希望在完全隔离的情况下超越测试单个类,但同时又避免在Java EE中运行完整的集成测试(或者我应该说, Jakarta EE )容器。
这是说企业应用程序的所有测试都应该以所描述的方式实现吗?当然不是。纯单元测试是一个很好的选择,以确定单个类的正确内部功能。完整的端到端集成测试非常有意义,可以确保应用程序的所有部分和层从上到下正确地协同工作。但是建议的替代方案可以是一个非常有用的工具,以确保业务逻辑和持久层的正确交互,而不会产生容器部署的开销,其中包括测试正确的事务行为,事务观察器方法和使用CDI服务的实体监听器。
话虽如此,但为了实现这些测试,需要更少的胶水代码是可取的。虽然您可以在自定义JUnit规则中封装所需基础架构的管理,但理想情况下,这已经为我们提供了。所以我在Weld JUnit项目中打开 了一张票 ,讨论了在项目中创建单独的JPA / JTA模块的想法。只需将依赖项添加到此类模块,即可为您提供开始在Java SE下测试CDI bean和持久层所需的一切。如果您对此感兴趣或者甚至想对此工作,请务必与Weld团队取得联系。
您可以在我们的 示例存储库中 找到此博客文章的完整源代码。您的反馈非常受欢迎,只需在下面添加评论即可。期待您的回音!