在 JUnit 5 中所有類別的所有測試之前執行程式碼
1.概述
在本教程中,我們將探討如何在 JUnit 5 中跨多個類別的所有測試之前執行一次設定程式碼。這對於全域初始化特別有用,例如設定共用資料庫連線、載入昂貴的資源或配置所有測試所依賴的環境。
JUnit 5 提供了類似@BeforeAll
生命週期註解,用於每個類別的設置,但它缺少真正的「全域之前」執行的內建機制。我們將討論標準註解的局限性,並探討使用擴充和監聽器的實際解決方案。這些方法確保即使在平行測試環境中也能一次完成設定和拆卸。
2. 了解 JUnit 5 生命週期註釋
我們先來簡單介紹一下 JUnit 的生命週期。 JUnit 5 提供了豐富的註解來管理測試生命週期,讓我們能夠在各個層面控制測試的啟動和卸載。
2.1. @BeforeAll
的作用
@BeforeAll
註解標記某個靜態方法在單一類別中的所有@Test
方法之前執行一次。它非常適合特定於類別的設置,例如初始化共享狀態。
這是一個基本的例子:
public class ExampleTest {
@BeforeAll
static void setup() {
System.out.println("Execute: BeforeAll");
// Initialize class-specific resources, eg, a mock database
}
@Test
void test1() {
System.out.println("Execute test 1");
// Test logic
}
@Test
void test2() {
System.out.println("Execute test 2");
// Test logic
}
}
這會為每個類別運行一次setup()
函數。此測試文件的輸出如下:
Execute: BeforeAll
Execute test 2
Execute test 1
但對於全域、一次性執行,單靠@BeforeAll
是不夠的。
2.2. 全球執行的限制
雖然@BeforeAll
在類別中運作良好,但 JUnit 5 並未提供套件範圍內設定的直接等效方法。主要限制包括:
- 每個類別的範圍:它與各個類別相關,導致多類別套件中的冗餘執行。
- 沒有內建套件支援:JUnit 5 強調模組化而非傳統套件,因此全域掛鉤需要自訂實作。
- 平行性挑戰:在平行測試運行中(透過
junit.jupiter.execution.parallel.enabled=true
啟用),如果沒有正確同步,共享資源的並發存取可能會導致問題。
這些差距需要擴展或監聽器等高階功能來實現真正的全域行為。
3. 使用 JUnit 擴充實現全域設定
JUnit 5 的擴充模型是一種將自訂行為注入測試生命週期的強大方法。我們可以創建一個擴展,透過確保程式碼只運行一次來模擬整個測試套件的設定。
3.1. 建立自訂擴展
讓我們實作BeforeAllCallback
來掛載到每個類別之前的生命週期並使用標誌進行一次性執行。
這是資料庫設定的類別版本:
public class DatabaseSetupExtension implements BeforeAllCallback {
private static boolean initialized = false;
@Override
public void beforeAll(ExtensionContext context) throws Exception {
if (!initialized) {
initialized = true;
// Global setup: Initialize database connections
System.out.println("Initializing global database connections...");
// Example: DatabaseConnectionPool.initialize();
}
}
}
JUnit 5 會在應用BeforeAllCallback
擴充的每個測試類別中呼叫一次該擴充的beforeAll()
方法。如果我們的測試套件包含多個測試類, beforeAll()
可能會被呼叫多次。因此,我們需要initialized,
該變數充當擴展所有實例共享的標誌。它確保設定程式碼僅在第一次呼叫beforeAll()
時運行。
為了處理並行測試,讓我們新增同步:
// Add to the class
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void beforeAll(ExtensionContext context) throws Exception {
lock.lock();
try {
if (!initialized) {
// Setup code...
}
} finally {
lock.unlock();
}
}
使用鎖可以確保我們的程式碼是線程安全的。
現在我們有兩個版本的擴展,但它們只有在我們啟用它們並讓 JUnit 識別它們時才會執行任何操作。讓我們看看如何操作。
3.2. 註冊擴充套件
為了全域應用擴充而不註釋每個類,我們必須完成兩個步驟。
首先,我們在src/test/resources/junit-platform.properties
中啟用自動偵測:
junit.jupiter.extensions.autodetection.enabled = true
然後,我們透過建立src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
檔案來註冊擴展,內容如下:
com.baeldung.before.all.global.DatabaseSetupExtension
就是這樣!現在執行ExampleTest
時,我們得到以下輸出:
Initializing global database connections...
Execute: BeforeAll
Execute test 2
Execute test 1
或者,我們可以用@ExtendWith(DatabaseSetupExtension.class)
註解單一類別。此標誌可以防止多次執行。
這種方法非常靈活,並且可以與 JUnit 5 的生命週期無縫整合。
4. 替代方法
雖然擴充是最常見的解決方案,但根據我們的設置,其他機制也可以實現類似的結果。
4.1. 使用TestExecutionListener
為了更底層地鉤住整個測試計劃,我們實作了[TestExecutionListener](https://docs.junit.org/5.0.3/api/org/junit/platform/launcher/TestExecutionListener.html) .
它會在任何測試之前執行testPlanExecutionStarted()
,並在所有測試之後testPlanExecutionFinished()
。
以下是我們如何編寫這樣的類別:
public class GlobalDatabaseListener implements TestExecutionListener {
@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
// Global setup
System.out.println("GlobalDatabaseListener # testPlanExecutionStarted ");
// Example: DatabaseConnectionPool.initialize();
}
@Override
public void testPlanExecutionFinished(TestPlan testPlan) {
// Global teardown
System.out.println("GlobalDatabaseListener # testPlanExecutionFinished");
// Example: DatabaseConnectionPool.shutdown();
}
}
與擴充功能類似,我們必須透過建立檔案src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
來註冊它,內容如下:
com.baeldung.before.all.global.GlobalDatabaseListener
完成後,這是我們的測試的輸出:
GlobalDatabaseListener # testPlanExecutionStarted
Initializing global database connections...
Execute: BeforeAll
Execute test 2
Execute test 1
GlobalDatabaseListener # testPlanExecutionFinished
我們可以看到, GlobalDatabaseListener
首先透過testPlanExecutionStarted()
方法執行。當所有其他測試函數完成後,才會呼叫testPlanExecutionFinished()
方法──這正是我們想要的。
4.2. 使用LauncherSessionListener
為了對啟動器會話進行更高層級的控制(如果適用,則包含多個測試計劃),我們實作了LauncherSessionListener
。這在具有多個啟動項目的環境中非常有用,例如 IDE:
public class GlobalDatabaseSessionListener implements LauncherSessionListener {
@Override
public void launcherSessionOpened(LauncherSession session) {
// Global setup before session starts
System.out.println("launcherSessionOpened");
}
@Override
public void launcherSessionClosed(LauncherSession session) {
// Global teardown after session ends
System.out.println("launcherSessionClosed");
}
}
為了使其工作,我們還必須註冊它,透過建立一個檔案src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener
其內容如下:
com.baeldung.before.all.global.GlobalDatabaseSessionListener
當我們再次運行測試時,我們得到以下輸出:
launcherSessionOpened
GlobalDatabaseListener # testPlanExecutionStarted
Initializing global database connections...
Execute: BeforeAll
Execute test 2
Execute test 1
GlobalDatabaseListener # testPlanExecutionFinished
launcherSessionClosed
我們可以看到所有執行的測試,包括三個不同的「before-globally」實現的包裝語句。
5. 潛在陷阱和最佳實踐
在實施全域設定時(我們的任何實作),都應考慮以下陷阱和提示:
- 執行緒安全:在並行環境中始終同步共享資源以避免競爭情況。
- 資源洩漏:確保拆卸正確註冊;在故障場景中進行測試。
- 測試隔離:全域設定可能會違反隔離 - 使用
@BeforeEach
/@AfterEach
中的交易或重置進行每次測試清理。 - 建置工具相容性:驗證 Maven/Gradle 中的行為;使用 Surefire 等插件進行平行控制。
- 偵錯:首先廣泛記錄並依序執行測試(
junit.jupiter.execution.parallel.enabled=false
)以隔離問題。 - 效能:盡量減少全域設定時間以避免降低套件速度。
遵循這些可確保測試可靠、可維護。
6. 結論
在本文中,我們了解到 JUnit 5 的模組化設計需要創造性的解決方案來進行全域設置,但擴充和監聽器提供了強大的選項。自訂擴充方法提供了細粒度的控制,而監聽器則在更高層次上處理套件範圍內的需求。
我們應該根據專案的複雜性進行選擇:大多數情況下選擇擴展,更廣泛範圍的選擇監聽器。
與往常一樣,程式碼可在 GitHub 上取得。