在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團隊取得聯絡。
您可以在我們的示例儲存庫中 找到此部落格文章的完整原始碼。您的反饋非常受歡迎,只需在下面新增評論即可。期待您的迴音!