1. 概述
main()
方法是每个 Java 应用程序的入口点,根据应用类型的不同,其实现方式也会有所差异。对于常规 Web 应用,main()
方法主要负责启动上下文;而对于某些控制台应用,我们可能会直接在其中编写业务逻辑。
测试 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 的 Option builder 定义 "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
基于解析字符串计算总和
这种分离使 main 类保持简单:
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
)单独进行单元测试。
但对于有历史遗留的老应用,情况会更复杂。尤其当开发者将大量业务逻辑直接放在 main 类的静态上下文中时。遗留代码往往难以修改,我们只能基于现有代码进行测试。
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);
也可用长参数形式调用:
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. 完整测试示例
结合前文知识编写完整测试,步骤如下:
- 将 main 类模拟为 Spy
- 将输入参数定义为
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();
}
6. 总结
本文探讨了多种 main 方法设计及其对应的测试方法,涵盖:
- 静态方法测试
- void 方法测试
- 参数处理
- 默认系统流更改
完整示例可在 GitHub 获取。