转载

在Java SE下测试CDI Bean和持久层 - relation

在测试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 ):

<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

  • 注册JTA同步(用于使事务观察器方法工作),
  • 查询当前的交易状态和
  • 获取用户事务(以便启用UserTransaction对象的注入)。

该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团队取得联系。

您可以在我们的 示例存储库中 找到此博客文章的完整源代码。您的反馈非常受欢迎,只需在下面添加评论即可。期待您的回音!​​​​​​​

原文  https://www.jdon.com/51714
正文到此结束
Loading...