在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 ... } } |
我們還可以使用J他的@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團隊取得聯絡。
您可以在我們的示例儲存庫中找到此部落格文章的完整原始碼。您的反饋非常受歡迎,只需在下面新增評論即可。期待您的迴音!
相關文章
- Java SE 檔案上傳和檔案下載的底層原理Java
- Java資料持久層Java
- Java 持久層框架之 MyBatisJava框架MyBatis
- Java持久層框架Mybatis入門Java框架MyBatis
- JAVA CDI @Inject基本用法Java
- MicroStream + Helidon高效能Java持久層ROSJava
- 小米9 SE玩吃雞卡不卡?小米9 SE跑分和遊戲效能測試遊戲
- JAVA CDI 學習- @Produces及@DisposesJava
- Spring 持久層整合Spring
- cglib、orika、spring等bean copy工具效能測試和原理分析CGLibSpringBean
- Java SE, Java EE, Java MEJava
- 你在測試金字塔的哪一層?(上)
- java,netcore和nodejs api效能測試JavaNetCoreNodeJSAPI
- Linux測試上行和下載速率Linux
- 小米8 SE評測:優點和槽點明顯 小米8 SE值得買嗎?
- 簡單談一下我對持續測試下的測試左移、迭代測試和測試右移的理解吧
- Java SE 22 新增特性Java
- Java SE 21 新增特性Java
- Java SE 20 新增特性Java
- Java se 複習05Java
- Java SE 23 新增特性Java
- JAVA SE基礎(二)Java
- ABAP和Java SpringBoot的單元測試JavaSpring Boot
- Java Bean ValidationJavaBean
- 藉助Docker,在win10下編碼,一鍵在Linux下測試DockerWin10Linux
- Golang 單元測試 - 介面層Golang
- 河青的持久層框架hqbatis框架BAT
- Java(3)-POJO和Java bean的區別是什麼JavaPOJOBean
- Java Platform SE 8(Java™程式語言)JavaPlatform
- Java XML和JSON:Java SE的文件處理,第1部分JavaXMLJSON
- Java XML和JSON:Java SE的文件處理 第2部分JavaXMLJSON
- What is an SQL relation?SQL
- JAVA面試題:Spring中bean的生命週期Java面試題SpringBean
- Java SE 語法學習Java
- ABAP和Java的單元測試Unit TestJava
- 效能測試工具Lmbench的使用和下載
- Spring Boot單元測試之服務層測試總結Spring Boot
- Golang 單元測試 - 資料層Golang