使用 JUnit 測試 Main 方法
1. 概述
main()
方法可作為每個 Java 應用程式的起點,根據應用程式類型,它可能看起來有所不同。對於常規 Web 應用程序, main()
方法將負責上下文啟動,但對於某些控制台應用程序,我們會將業務邏輯放入其中。
測試main()
方法非常複雜,因為我們有一個靜態方法,它只接受字串參數並且不傳回任何內容。
在本文中,我們將了解如何測試主要方法,重點關注命令列參數和輸入流。
2.Maven依賴
對於本教學課程,我們需要幾個測試函式庫(Junit 和 Mockito)以及 Apache Commons CLI 才能使用參數:
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
我們可以在 Maven 中央儲存庫中找到最新版本的JUnit 、 Mockito和Apache Commons CLI 。
3. 設定場景
為了說明main()
方法測試,讓我們定義一個實際場景。想像一下,我們的任務是開發一個簡單的應用程序,旨在計算所提供數字的總和。它應該能夠從控制台或檔案中互動地讀取輸入,具體取決於提供的參數。程式輸入包含一系列數字。
根據我們的場景,程式應該根據使用者定義的參數動態調整其行為,從而執行不同的工作流程。
3.1.使用 Apache Commons CLI 定義程式參數
我們需要為所描述的場景定義兩個基本參數:「i」和「f」。 「i」選項指定具有兩個可能值(FILE 和 CONSOLE)的輸入來源。同時,「f」選項允許我們指定要讀取的檔案名,並且僅當「i」選項指定為FILE時才有效。
為了簡化我們與這些參數的交互,我們可以依賴Apache Commons CLI
函式庫。該工具不僅可以驗證參數,還可以方便值解析。以下是如何使用 Apache 的選項建構器定義「i」選項的說明:
Option inputTypeOption = Option.builder("i")
.longOpt("input")
.required(true)
.desc("The input type")
.type(InputType.class)
.hasArg()
.build();
一旦我們定義了選項, Apache Commons CLI
將協助解析輸入參數以分支業務邏輯的工作流程:
Options options = getOptions();
CommandLineParser parser = new DefaultParser();
CommandLine commandLine = parser.parse(options, args);
if (commandLine.hasOption("i")) {
System.out.print("Option i is present. The value is: " + commandLine.getOptionValue("i") + " \n");
String optionValue = commandLine.getOptionValue("i");
InputType inputType = InputType.valueOf(optionValue);
String fileName = null;
if (commandLine.hasOption("f")) {
fileName = commandLine.getOptionValue("f");
}
String inputString = inputReader.read(inputType, fileName);
int calculatedSum = calculator.calculateSum(inputString);
}
為了保持清晰和簡單,我們將職責分為不同的類別。 InputType
枚舉封裝了可能的輸入參數值。 InputReader
類別根據InputType
檢索輸入字串,而Calculator
根據解析的字串計算總和。
有了這樣的分離,我們可以保留一個簡單的主類,如下所示:
public static void main(String[] args) {
Bootstrapper bootstrapper = new Bootstrapper(new InputReader(), new Calculator());
bootstrapper.processRequest(args);
}
4. 如何測試Main方法
main()
方法的簽章和行為與我們在應用程式中使用的常規方法不同。因此,我們需要結合多種特定於測試靜態方法、void 方法、輸入流和參數的測試策略。
我們將在下面的段落中介紹每個概念,但讓我們先看看如何建立main()
方法的業務邏輯。
當我們開發一個新的應用程式時,我們可以完全控制它的架構,那麼main()
方法不應該有任何複雜的邏輯,而不是初始化所需的工作流程。有了這樣的架構,我們就可以對每個工作流程部分進行適當的單元測試( Bootstrapper
、 InputReader
和Calculator
可以單獨測試)。
另一方面,當涉及到具有歷史記錄的舊應用程式時,事情可能會變得有點棘手。尤其是當先前的開發人員將大量業務邏輯直接放置在主類別的靜態情境中時。遺留程式碼並不總是可以更改,我們應該使用已經編寫的內容。
4.1.如何測試靜態方法
過去,使用 Mockito 處理靜態情境是一個相當大的挑戰,通常需要使用PowerMockito等函式庫。然而,在最新版本的 Mockito 中,這個限制已經被克服。隨著3.4.0版本中Mockito.mockStatic的引入,我們現在可以輕鬆地模擬和驗證靜態方法,而無需額外的函式庫。這項增強簡化了涉及靜態方法的測試場景,使我們的測試過程更加精簡和高效。
使用MockedStatic
我們可以執行與常規 Mock 相同的操作:
try (MockedStatic<SimpleMain> mockedStatic = Mockito.mockStatic(StaticMain.class)) {
mockedStatic.verify(() -> StaticMain.calculateSum(stringArgumentCaptor.capture()));
mockedStatic.when(() -> StaticMain.calculateSum(any())).thenReturn(24);
}
為了強制MockedStatic
作為 Spy 工作,我們需要加入一個配置參數:
MockedStatic<StaticMain> mockedStatic = Mockito.mockStatic(StaticMain.class, Mockito.CALLS_REAL_METHODS)
一旦我們根據需要配置了MockedStatic
,我們就可以徹底測試靜態方法。
4.2.如何測試 void 方法
遵循功能開發方法,方法應符合幾個要求。它們應該是獨立的,不應該修改傳入的參數,並且應該傳回處理結果。
透過這種行為,我們可以輕鬆地根據返回結果驗證編寫單元測試。然而,測試 void 方法則不同,焦點轉移到方法執行引起的副作用和狀態變化。
4.3.如何測試程序參數
我們可以像任何其他標準 Java 方法一樣從測試類別呼叫main()
方法。要使用不同的參數集評估其行為,我們只需在呼叫期間提供這些參數。
考慮到上一段中的Options
定義,我們可以使用一個短參數-i
來呼叫main()
:
String[] arguments = new String[] { "-i", "CONSOLE" };
SimpleMain.main(arguments);
另外,我們可以使用長形式的-i
參數來呼叫 main 方法:
String[] arguments = new String[] { "--input", "CONSOLE" };
SimpleMain.main(arguments);
4.4.如何測試資料輸入流
從控制台讀取通常是用System.in
建構的:
private static String readFromConsole() {
System.out.println("Enter values for calculation: \n");
return new Scanner(System.in).nextLine();
}
System.in
是主機環境指定的「標準」輸入流,通常對應於鍵盤輸入。我們無法在測試中提供鍵盤輸入,但我們可以更改System.in
引用的串流類型:
InputStream fips = new ByteArrayInputStream("1 2 3".getBytes());
System.setIn(fips);
在此範例中,我們更改了預設輸入類型,以便應用程式將從ByteArrayInputStream
讀取並且不會繼續等待使用者輸入。
我們可以在測試中使用任何其他類型的InputStream
,例如,我們可以從檔案中讀取:
InputStream fips = getClass().getClassLoader().getResourceAsStream("test-input.txt");
System.setIn(fips);
此外,使用相同的方法,我們可以替換輸出流以驗證程式寫入的內容:
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintStream out = new PrintStream(byteArrayOutputStream);
System.setOut(out);
使用這種方法,我們將看不到控制台輸出,因為System.out
會將所有資料傳送到ByteArrayOutputStream
而不是控制台。
5. 完整的測試範例
讓我們結合前面段落中收集的所有知識來編寫完整的測試。這些是我們要執行的步驟:
- 嘲笑我們的主階級是間諜
- 將輸入參數定義為
String
數組 - 替換
System.in
中的預設流 - 驗證程式是否在靜態上下文中呼叫所有必要的方法,或者程式是否將必要的結果寫入控制台。
- 將
System.in
和System.out
流替換回原始流,以便流替換不會影響其他測試
在此範例中,我們對StaticMain
類別進行了測試,其中所有邏輯都放置在靜態上下文中。我們用ByteArrayInputStream
取代System.in
並基於verify():
@Test
public void givenArgumentAsConsoleInput_WhenReadFromSubstitutedByteArrayInputStream_ThenSuccessfullyCalculate() throws IOException {
String[] arguments = new String[] { "-i", "CONSOLE" };
try (MockedStatic mockedStatic = Mockito.mockStatic(StaticMain.class, Mockito.CALLS_REAL_METHODS);
InputStream fips = new ByteArrayInputStream("1 2 3".getBytes())) {
InputStream original = System.in;
System.setIn(fips);
ArgumentCaptor stringArgumentCaptor = ArgumentCaptor.forClass(String.class);
StaticMain.main(arguments);
mockedStatic.verify(() -> StaticMain.calculateSum(stringArgumentCaptor.capture()));
System.setIn(original);
}
}
我們可以對SimpleMain
類別使用稍微不同的策略,因為在這裡我們透過其他類別分發了所有業務邏輯。
在這種情況下,我們甚至不需要模擬SimpleMain
類,因為裡面沒有其他方法。我們將System.in
替換為檔案流,並根據傳播到ByteArrayOutputStream
控制台輸出建立驗證:
@Test
public void givenArgumentAsConsoleInput_WhenReadFromSubstitutedFileStream_ThenSuccessfullyCalculate() throws IOException {
String[] arguments = new String[] { "-i", "CONSOLE" };
InputStream fips = getClass().getClassLoader().getResourceAsStream("test-input.txt");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintStream out = new PrintStream(byteArrayOutputStream);
System.setIn(fips);
System.setOut(out);
SimpleMain.main(arguments);
String consoleOutput = byteArrayOutputStream.toString(Charset.defaultCharset());
assertTrue(consoleOutput.contains("Calculated sum: 10"));
fips.close();
out.close();
}
六,結論
在本文中,我們探討了幾種主要方法設計及其對應的測試方法。我們已經介紹了靜態和 void 方法的測試、處理參數以及更改預設系統流。
完整的範例可以在 GitHub 上找到。