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

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团队取得联系。
您可以在我们的示例存储库中找到此博客文章的完整源代码。您的反馈非常受欢迎,只需在下面添加评论即可。期待您的回音!​​​​​​​