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