解決 JUnit 錯誤:測試類別應該只有一個公共的無參構函數
1. 概述
在 Java 中編寫單元測試時,我們可能會遇到 JUnit 錯誤: Test class should have exactly one public zero-argument constructor 。這通常發生在 JUnit 4 中,因為我們的測試類別定義了一個帶有參數的建構子。
本教程將重點介紹如何解決該錯誤。在接下來的章節中,我們將展示如何透過使用參數化測試、升級到 JUnit 5 或避免使用參數化建構函式來修復該錯誤。
2. 理解錯誤
在執行任何測試方法之前,JUnit 通常會建立一個新的測試類別實例,以便每個測試都能獨立執行,並且不會與其他測試共用狀態。具體來說,在 JUnit 4 中,這個過程依賴於找到一個公共的無參構造函數。
通常情況下,如果一個類別沒有定義建構函數,Java 會自動提供一個公共的無參構造函數。這個隱式建構函式旨在幫助 JUnit 4 使用反射來實例化測試類別。
然而,一旦我們定義了自己的建構函數,特別是接受參數的建構函數,Java 就會停止產生預設建構函數。這可以確保在建立物件時不會出現呼叫哪個建構函式的混淆。發生這種情況時,JUnit 4 將無法再呼叫new TestClass()因為預設建構子已不存在,從而拋出上述錯誤:
public class ResolvingJUnitConstructorErrorUnitTest {
private int input;
// Constructor with a parameter (causes the error)
public ResolvingJUnitConstructorErrorUnitTest(int input) {
...
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
...
}
}
以上述範例為例,JUnit 4 無法執行測試,因為它不知道建構函式中的int參數應該取什麼值ResolvingJUnitConstructorErrorUnitTest )。由於我們沒有使用無參建構函數,JUnit 4 無法在執行測試方法之前自動建立實例。
JUnit 依賴無參建構子是有意為之。當我們始終建立一個新的、無參實例時,JUnit 可以確保每個測試獨立運行,從而消除共享狀態的風險。例如,它可以防止一個測試修改另一個測試後續依賴的欄位。透過這種隔離,測試可以保證結果的可預測性和可重複性。
如果像範例中那樣引入帶有參數的建構函數,就會破壞受控的生命週期。因此,JUnit 無法再自動建構測試類,並拋出“Test class should have exactly one public zero-argument constructor”錯誤。
3. 重現錯誤
在本節中,我們可以使用一個最小的 Maven 專案來重現該錯誤。之後,讓我們在專案的pom.xml檔案中加入 JUnit 4 作為依賴項:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
上面的程式碼區塊加入了JUnit作為依賴項。
現在讓我們建立主類別ResolvingJUnitConstructorError :
public class ResolvingJUnitConstructorError {
public int square(int a) {
return a * a;
}
}
該類別中有一個square()方法,用於傳回一個數的平方。
既然我們的主類別已經準備就緒,現在讓我們開始編寫測試類別:
public class ResolvingJUnitConstructorErrorUnitTest {
private int input;
public ResolvingJUnitConstructorErrorUnitTest(int input) {
this.input = input;
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
ResolvingJUnitConstructorError service = new ResolvingJUnitConstructorError();
assertEquals(input * input, service.square(input));
}
}
在這裡,我們定義了一個名為ResolvingJUnitConstructorErrorUnitTest的 JUnit 4測試,它包含一個接受輸入參數的建構子。
最後,我們來執行一下測試:
$ mvn clean test
...
1. Test class should have exactly one public zero-argument constructor
...
一旦我們這樣做,構建就會在任何測試方法運行之前停止,最終我們會得到預期的錯誤訊息“ Test class should have exactly one public zero-argument constructor 。
4. 解決 JUnit 4 中的錯誤
接下來,我們來看第一個解決方案。在這個方案中,我們在 JUnit 4 中實作了@RunWith(Parameterized.class)註解,以便自動將參數傳遞給測試類別的建構子:
@RunWith(Parameterized.class)
public class ResolvingJUnitConstructorErrorUnitTest {
private final int input;
private final ResolvingJUnitConstructorError service = new ResolvingJUnitConstructorError();
public ResolvingJUnitConstructorErrorUnitTest(int input) {
this.input = input;
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{2}, {3}, {4}
});
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
assertEquals(input * input, service.square(input));
}
}
上面,我們將測試類別更新為使用 JUnit 4的參數化測試:
-
@RunWith(Parameterized.class): 指示 JUnit 使用參數化運行器。 -
@Parameterized.Parameters:定義輸入集合,在本例中為{ 2 }, { 3 }, { 4 }
讓我們看看再次運行測試會發生什麼:
$ mvn clean test
...
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
...
因此,上面的輸出表明我們的測試現在運行了三次。具體來說,每個輸入值都會執行一次測試。對於每個資料集( {2}, {3}, {4} ),JUnit 4 都會建立一個新的測試類別實例,並分別對每個實例執行測試,將它們視為獨立的執行過程。
使用@RunWith(Parameterized.class)註解,JUnit 4 現在會為每組輸入資料建立一個新的測試類別實例。每個實例都透過我們定義的建構子接收其參數。
在這裡,我們修復了這個問題,並展示了 JUnit 4 如何透過基於建構函數的注入來實現參數化測試。
5. 透過升級到 JUnit 5解決此問題
接下來,我們來看第二個解決方案。在這個方案中,我們將 JUnit 4 升級到 JUnit 5 :
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.14.1</version>
<scope>test</scope>
</dependency>
</dependencies>
具體來說,我們新增了依賴項JUnit Jupiter 。
現在,讓我們重寫測試:
public class ResolvingJUnitConstructorErrorUnitTest {
private final ResolvingJUnitConstructorError service = new ResolvingJUnitConstructorError();
@ParameterizedTest
@ValueSource(ints = {2, 3, 4})
void givenNumber_whenSquare_thenReturnsCorrectResult(int input) {
assertEquals(input * input, service.square(input));
}
}
在更新後的範例中,JUnit 5使用@ParameterizedTest和@ValueSource簡化了參數化測試:
-
@ParameterizedTest:將測試標記為參數化測試。 -
@ValueSource(ints = {2, 3, 4}): 定義內嵌測試數據
現在我們來執行測試:
$ mvn clean test
...
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
...
上面的輸出顯示,測試運行了三次,與之前相同,並且全部成功通過。每個輸入都會觸發一次單獨的測試執行,確保測試運行之間的隔離,這與 JUnit 4 相同。
預設情況下,JUnit 5支援參數化測試,無需額外配置。因此,它省去了公共無參構造函數的需要,從而使我們的測試程式碼更加簡潔易讀。
6. 透過避免使用帶參數的建構函數來解決問題
接下來,我們來看第三種方案。在這種方案中,測試類別不使用帶有參數的建構子。具體來說,我們不是透過建構函式傳遞值,而是在 setup 方法中初始化測試輸入。
我們先用 JUnit 4 來舉例:
public class ResolvingJUnitConstructorErrorUnitTest {
private ResolvingJUnitConstructorError service;
private int input;
@Before
public void setUp() {
service = new ResolvingJUnitConstructorError();
input = 2;
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
assertEquals(input * input, service.square(input));
}
}
現在讓我們來看一個 JUnit 5 的範例:
public class ResolvingJUnitConstructorErrorUnitTest {
private ResolvingJUnitConstructorError service;
private int input;
@BeforeEach
void setUp() {
service = new ResolvingJUnitConstructorError();
input = 2;
}
@Test
void givenNumber_whenSquare_thenReturnsCorrectResult() {
assertEquals(input * input, service.square(input));
}
}
在這兩個範例中,我們分別使用了 JUnit 4 中的@Before和 JUnit 5 中的@BeforeEach設定方法。需要說明的是,這兩個方法都會在每次測試運行之前初始化service和input欄位。
現在,每個測試方法都可以專注於驗證預期結果,而無需關注初始化過程。此外,我們現在可以將測試準備與執行分離,使測試方法能夠專注於驗證行為。
7. 常見錯誤和最佳實踐
在重構或引入依賴項的過程中,我們可能會不小心在測試類別中新增帶有參數的建構函式。在這種情況下,我們可以應用前面討論過的 JUnit 4或 JUnit 5的解決方案來避免Test class should have exactly one public zero-argument constructor錯誤。
有時,我們可能會無意中重複使用類似的測試方法,這些方法僅在輸入值上略有不同。在這種情況下,我們可以考慮使用參數化測試,而不是重複編寫相同的測試。
我們也可以繼續使用 JUnit 4編寫測試,即使與替代方案相比,這可能需要編寫更多樣板程式碼。當處理需要大量測試的大型專案時,遷移到 JUnit 5可以簡化參數化測試的編寫,並減少重複的設定工作。
8. 結論
在本文中,我們研究了 JUnit 錯誤Test class should have exactly one public zero-argument constructor 。
此錯誤通常發生在 JUnit 4中,當測試類別定義了帶有參數的建構函數時。在這種情況下,JUnit 4無法在執行測試之前自動建立類別的實例。為了示範此問題,我們重現了這個問題,解釋了其發生的原因,並提供了切實可行的解決方案。
在我們的第一個解決方案中,我們使用 JUnit 4中的參數化測試並控制測試資料的注入;而在第二個解決方案中,我們升級到 JUnit 5 ,並提供了更簡潔的方法。在第三個解決方案中,我們摒棄了參數化建構函數,轉而依賴 setup 方法,這使得我們的測試更簡單、更容易維護。透過理解錯誤,我們不僅能夠修復測試失敗,還能加深對 JUnit 如何管理測試實例化和生命週期的理解。
我們可以從 GitHub 上取得原始碼。