JUnit 中的 @ClassTemplate 註解簡介
1.概述
JUnit Jupiter 最近引入了[@ClassTemplate](https://docs.junit.org/current/api/org.junit.jupiter.api/org/junit/jupiter/api/ClassTemplate.html)
註解,它允許我們在不同的呼叫上下文中多次執行整個測試類別。我們無需重複設定程式碼或將參數分散到多個方法中,而是可以使用註冊提供者傳回的自訂上下文來執行整個類別的執行。
在本教程中,我們將介紹一個簡潔實用的範例,該範例在兩個上下文中運行同一個測試類別:一個用於英語,一個用於義大利語。這種方法使程式碼保持簡潔,並揭示了其用途。
2. 先決條件
需要 Java 17 或更高版本。
我們必須記住, @ClassTemplate
批註最近才在 JUnit 5.13.x 中引入,因此我們必須使用最新版本的JUnit Jupiter 、 JUnit Launcher和Maven Surefire 。此pom.xml
檔顯示如何執行此操作:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
</plugin>
</plugins>
</build>
我們可以檢查Maven 儲存庫,看看是否有更新的版本。
3. 建構類別模板
使用@ClassTemplate
標記測試類別會告知 JUnit,該類別並非傳統的測試容器。相反,它是一個模板,其執行次數取決於一個或多個已註冊的ClassTemplateInvocationContextProvider
類別提供的呼叫上下文。如果沒有註冊任何提供程序,測試將失敗,因此我們必須始終將類別模板與至少一個提供程序配對。
但首先,讓我們從域類別開始。
3.1. 領域類
我們的領域類別只是一個Greeter
,它根據語言代碼傳回本地化的問候語:
public class Greeter {
public String greet(String name, String language) {
return "it".equals(language) ? "Ciao " + name : "Hello " + name;
}
}
這將幫助我們清楚地看到英語和義大利語參數化之間的差異。
3.2. 呼叫上下文提供者
在底層,提供者介面公開了兩種關鍵方法:
-
supportsClassTemplate(ExtensionContext …)
,確定是否要處理模板 -
provideClassTemplateInvocationContexts(ExtensionContext …)
,它提供ClassTemplateInvocationContext
物件流,每次執行一個
每個ClassTemplateInvocationContext
都提供一個ParameterResolver
,它將語言程式碼注入GreeterClassTemplateUnitTest
建構子並為測試設定一個描述性名稱:
public class GreeterClassTemplateInvocationContextProvider
implements ClassTemplateInvocationContextProvider {
@Override
public boolean supportsClassTemplate(ExtensionContext context) {
return context.getTestClass()
.map(c -> c.isAnnotationPresent(ClassTemplate.class))
.orElse(false);
}
@Override
public Stream<ClassTemplateInvocationContext> provideClassTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(contextFor("en"), contextFor("it"));
}
private ClassTemplateInvocationContext contextFor(String language) {
ParameterResolver resolver = new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.getParameter().getType() == String.class;
}
@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext ec) {
return language;
}
};
return new ClassTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return "Language-" + language;
}
@Override
public List<Extension> getAdditionalExtensions() {
return List.of(resolver);
}
};
}
}
補充一下, getDisplayName(…)
方法目前在mvn test
中沒有被考慮。為了解決這個問題,我們可以在GreeterClassTemplateUnitTest
中插入一個明確的日誌,以便了解測試的上下文。
3.3. 類別模板測試
現在,我們來看看測試類別。我們使用@ClassTemplate
將其標記為類別模板,並使用@ExtendWith(…)
註冊提供者。該類別在其建構函數中接受語言程式碼作為參數,並且提供者會在每次呼叫時注入此值:
@ClassTemplate
@ExtendWith(GreeterClassTemplateInvocationContextProvider.class)
class GreeterClassTemplateUnitTest {
private static final Logger LOG =
System.getLogger("GreeterClassTemplateUnitTest");
private final String language;
GreeterClassTemplateUnitTest(String language) {
this.language = language;
}
@BeforeEach
void logContext() {
LOG.log(Level.INFO, () -> ">> Context: Language-" + language);
}
@Test
void whenGreet_thenLocalizedMessage() {
Greeter greeter = new Greeter();
String actual = greeter.greet("Baeldung", language);
assertEquals(
"it".equals(language) ? "Ciao Baeldung" : "Hello Baeldung",
actual
);
}
}
可選的@BeforeEach
方法透過指示上下文來闡明日誌。
現在,讓我們來看看@ClassTemplate
方法的一些優點:
- 類別範圍的變化:建構函數接收語言程式碼,因此類別中的每個測試都在該語言環境中運行
- 無重複:單一測試類別可產生多次運行,每個上下文一次,無需複製和貼上類別或依賴全域狀態
- 明確範圍:
@ClassTemplate
表明我們打算透過上下文多次運行該類,保持程式碼和報告整潔
預設情況下,呼叫按順序運行,每次在單一執行緒上執行一個上下文。在每個上下文中,JUnit 使用PER_METHOD
生命週期,為每個測試方法建立一個新的測試實例。但是,我們可以透過 JUnit 平台設定啟用並行執行。
3.4. 運行測試
讓我們檢查一下測試是否按預期進行:
$ mvn clean test
[...]
[INFO] -------------------------------------------------------
[INFO] TESTS
[INFO] -------------------------------------------------------
[INFO] Running com.baeldung.classtemplate.GreeterClassTemplateUnitTest
[INFO] Running com.baeldung.classtemplate.GreeterClassTemplateUnitTest
INFO: >> Context: Language-en
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.046 s -- in com.baeldung.classtemplate.GreeterClassTemplateUnitTest
[INFO] Running com.baeldung.classtemplate.GreeterClassTemplateUnitTest
INFO: >> Context: Language-it
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.baeldung.classtemplate.GreeterClassTemplateUnitTest
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.104 s -- in com.baeldung.classtemplate.GreeterClassTemplateUnitTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[...]
輸出正確,但可讀性不佳。此外,我們添加了@BeforeEach
方法來補充 Maven Surefire 插件日誌:這是一個簡單的解決方案,但並非理想的解決方案。
另一種方法是使用 JUnit Console Launcher,這是一個獨立的命令列工具,可以直接在 JUnit Platform 上執行測試。它的樹狀視圖更具可讀性,而且我們可以刪除@BeforeEach
方法:
$ mvn dependency:get -Dartifact=org.junit.platform:junit-platform-console-standalone:1.13.4
[...]
$ mvn clean -DskipTests package
[...]
$ java -jar ~/.m2/repository/org/junit/platform/junit-platform-console-standalone/1.13.4/junit-platform-console-standalone-1.13.4.jar \
--class-path target/test-classes:target/classes \
--scan-classpath \
--details tree
[...]
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│ └─ GreeterClassTemplateUnitTest ✔
│ ├─ Language-en ✔
│ │ └─ whenGreet_thenLocalizedMessage() ✔
│ └─ Language-it ✔
│ └─ whenGreet_thenLocalizedMessage() ✔
└─ JUnit Vintage ✔
[...]
這些命令適用於 Linux 和 macOS。在 Windows 上,我們使用%USERPROFILE%\.m2\repository,
將–class-path
中的:
替換為;
,並在一行上執行該指令或使用^
續行。
4. 結論
在本文中,我們建立了一個簡單而全面的範例,該範例使用@ClassTemplate
註解在兩個不同的語言環境中執行兩次測試類別。
我們觀察到ClassTemplateInvocationContextProvider
如何提供多個上下文,透過ParameterResolver
注入參數,並為每個呼叫分配可讀的顯示名稱。最終結果是清晰的報告和類別範圍內的差異,且不會出現重複。
簡而言之, @ClassTemplate
讓我們在不同的配置下運行整個測試類,同時保持測試簡單且富有表現力。
與往常一樣,完整程式碼可在 GitHub 上取得。