1. Overview
1.概述
The main() method serves as the starting point for each Java application, and it might look different depending on the app type. In the case of regular web applications, the main() method will be responsible for context start, but in the case of some console applications, we’ll put business logic to it.
main()方法是每个 Java 应用程序的起点,根据应用程序类型的不同,该方法可能会有所不同。对于普通 Web 应用程序,main() 方法将负责启动上下文,但对于某些控制台应用程序,我们将把业务逻辑放在其中。
Testing the main() method is quite complex because we have a static method that accepts only string arguments and returns nothing.
测试 main() 方法相当复杂,因为我们有一个只接受字符串参数且不返回任何内容的静态方法。
In this article, we’ll figure out how to test the main method focusing on command-line arguments and input streams.
在本文中,我们将了解如何测试主方法,重点是命令行参数和输入流。
2. Maven Dependencies
2.Maven 依赖项
For this tutorial, we’ll need several testing libraries (Junit and Mockito) as well as Apache Commons CLI in order to work with arguments:
在本教程中,我们需要几个测试库(Junit 和 Mockito)以及 Apache Commons CLI,以便使用参数:
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.6.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>
We can find the latest versions of JUnit, Mockito, and Apache Commons CLI in the Maven Central repository.
我们可以在 Maven Central 资源库中找到JUnit、Mockito和Apache Commons CLI的最新版本。
3. Setting Up a Scenario
3.设置情景
To illustrate main() method testing, let’s define a practical scenario. Imagine we’re tasked to develop a simple application that is designed to calculate the sum of provided numbers. It should be able to read an input, either interactively from the console or from a file, depending on the parameters provided. Program input comprises a series of numbers.
为了说明 main() 方法测试,让我们定义一个实际场景。假设我们的任务是开发一个简单的应用程序,用于计算所提供数字的总和。根据提供的参数,它应能从控制台或文件中交互读取输入。程序输入包括一系列数字。
Based on our scenario, the program should dynamically adapt its behavior based on user-defined parameters, leading to the execution of diverse workflows.
根据我们的设想,程序应根据用户定义的参数动态调整其行为,从而执行不同的工作流程。
3.1. Define Program Arguments With Apache Commons CLI
3.1.使用 Apache Commons CLI 定义程序参数
We need to define two essential arguments for the described scenario: “i” and “f”. The “i” option specifies the input source with two possible values (FILE and CONSOLE). Meanwhile, the “f” option allows us to specify a filename to read from, and it’s valid only in the case when the “i” option is specified as FILE.
我们需要为所述方案定义两个基本参数:”i “和 “f”。i “选项指定了两种可能的输入源(FILE 和 CONSOLE)。同时,”f “选项允许我们指定要读取的文件名,它只在 “i “选项指定为 FILE 时有效。
To streamline our interaction with these arguments, we can rely on the Apache Commons CLI library. This tool not only validates parameters but also facilitates value parsing. Here’s an illustration of how the ‘i’ option can be defined using Apache’s Option builder:
为了简化与这些参数的交互,我们可以使用 Apache Commons CLI 库。该工具不仅能验证参数,还能帮助我们解析参数值。下面是如何使用 Apache 选项生成器定义 “i “选项的示例:
Option inputTypeOption = Option.builder("i")
.longOpt("input")
.required(true)
.desc("The input type")
.type(InputType.class)
.hasArg()
.build();
Once we define our options, Apache Commons CLI will help to parse the input arguments to branch out the workflow of the business logic:
一旦我们定义了选项,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);
}
To maintain clarity and simplicity, we segregate responsibilities into different classes. The InputType enum encapsulates possible input parameter values. The InputReader class retrieves input strings based on InputType, and the Calculator computes sums based on parsed strings.
为了保持清晰和简洁,我们将责任划分为不同的类别。InputType 枚举封装了可能的输入参数值。InputReader 类根据 InputType 检索输入字符串,而 Calculator 则根据解析的字符串计算总和。
Having such separation allows us to keep a simple main class like this:
有了这样的分离,我们就可以保留这样一个简单的主类:
public static void main(String[] args) {
Bootstrapper bootstrapper = new Bootstrapper(new InputReader(), new Calculator());
bootstrapper.processRequest(args);
}
4. How to Test the Main Method
4.如何测试主要方法
The signature and behavior of the main() method are different from the regular methods we use in the app. Because of this, we need to combine multiple test strategies specific to testing static methods, void methods, input streams, and arguments.
main() 方法的签名和行为与我们在应用程序中使用的常规方法不同。因此,我们需要结合多种测试策略来测试静态方法、无效方法、输入流和参数。
We’ll cover each concept in the following paragraphs, but let’s first take a look at how the business logic of the main() method might be built.
我们将在下面的段落中介绍每个概念,但首先让我们看看如何构建 main() 方法的业务逻辑。
When we are working on a new application, and we can fully control its architecture, then the main() method should not have any complex logic rather than initializing a needed workflow. Having such architecture, we can perform proper unit testing of each workflow part (Bootstrapper, InputReader, and Calculator can be tested separately).
当我们正在开发一个新的应用程序,并且可以完全控制其架构时,main() 方法不应包含任何复杂的逻辑,而应初始化所需的工作流。有了这样的架构,我们就可以对每个工作流部分进行适当的单元测试(Bootstrapper、InputReader 和 Calculator 可以分别测试)。
On the other hand, when it comes to older applications with a history, things can get a bit trickier. Especially when previous developers have placed a lot of business logic directly in the main class’s static context. Legacy code is not always possible to change, and we should work with whatever is already written.
另一方面,当涉及到有历史的旧应用程序时,情况就会变得比较棘手。尤其是当以前的开发人员将大量业务逻辑直接放在主类的静态上下文中时。遗留代码并不总是可以修改的,我们应该利用已经编写的代码。
4.1. How to Test Static Methods
4.1.如何测试静态方法
In the past, dealing with static contexts with Mockito was quite a challenge, often requiring the use of libraries like PowerMockito. However, in the latest versions of Mockito, this limitation has been overcome. With the introduction of Mockito.mockStatic in the 3.4.0 version, we can now easily mock and verify static methods without the need for additional libraries. This enhancement simplifies testing scenarios involving static methods, making our testing process more streamlined and efficient.
在过去,使用 Mockito 处理静态上下文是一项相当大的挑战,通常需要使用 PowerMockito 等库。不过,在最新版本的 Mockito 中,这一限制已被克服。通过在 3.4.0 版本中引入 Mockito.mockStatic,我们现在可以轻松地模拟和验证静态方法,而无需使用额外的库。这一改进简化了涉及静态方法的测试场景,使我们的测试过程更加精简高效。
Using MockedStatic we can perform the same actions as with regular Mock:
使用 MockedStatic 我们可以执行与普通 Mock 相同的操作:
try (MockedStatic<SimpleMain> mockedStatic = Mockito.mockStatic(StaticMain.class)) {
mockedStatic.verify(() -> StaticMain.calculateSum(stringArgumentCaptor.capture()));
mockedStatic.when(() -> StaticMain.calculateSum(any())).thenReturn(24);
}
In order to force MockedStatic to work as a Spy, we need to add one configuration parameter:
为了强制 MockedStatic 作为 Spy 工作,我们需要添加一个配置参数:
MockedStatic<StaticMain> mockedStatic = Mockito.mockStatic(StaticMain.class, Mockito.CALLS_REAL_METHODS)
As soon as we configure MockedStatic based on our needs, we can thoroughly test static methods.
一旦我们根据需要配置了 MockedStatic 后,我们就可以彻底测试静态方法。
4.2. How to Test Void Methods
4.2.如何测试无效方法
Following functional development methodology, methods should conform to several requirements. They should be independent, should not modify incoming parameters, and should return processing results.
按照功能开发方法,方法应符合若干要求。它们应是独立的,不应修改输入参数,并应返回处理结果。
With such behavior, we can easily write unit tests based on returned result verification. However, testing void methods is different, and the focus shifts to the side effects and state changes caused by the method’s execution.
有了这些行为,我们就可以根据返回的结果验证轻松编写单元测试。然而,测试 void 方法则不同,重点将转移到方法执行时产生的副作用和状态变化上。
4.3. How to Test Program Arguments
4.3.如何测试程序参数
We can invoke the main() method from a test class in the same way as any other standard Java method. To evaluate its behavior with different parameter sets, we only need to provide these parameters during the call.
我们可以从测试类中调用 main() 方法,调用方式与其他标准 Java 方法相同。要在使用不同参数集时评估其行为,我们只需在调用时提供这些参数。
Considering the Options definition from the previous paragraph, we can call our main() with a short argument -i:
考虑到上一段中的 Options 定义,我们可以使用短参数 -i 调用 ain() :
String[] arguments = new String[] { "-i", "CONSOLE" };
SimpleMain.main(arguments);
Also, we can call the main method with the long form of the -i argument:
此外,我们还可以使用 -i 参数的长形式调用 main 方法:
String[] arguments = new String[] { "--input", "CONSOLE" };
SimpleMain.main(arguments);
4.4. How to Test Data Input Stream
4.4.如何测试数据输入流
Reading from the console is usually built with System.in:
通常使用 System.in 从控制台读取数据:
private static String readFromConsole() {
System.out.println("Enter values for calculation: \n");
return new Scanner(System.in).nextLine();
}
System.in it’s the “standard” input stream specified by the host environment, which usually corresponds to keyboard input. We cannot provide keyboard input in the test, but we can change the type of stream referenced by System.in:
System.in 是主机环境指定的 “标准 “输入流,通常与键盘输入相对应。我们无法在测试中提供键盘输入,但可以更改 System.in 引用的流类型:
InputStream fips = new ByteArrayInputStream("1 2 3".getBytes());
System.setIn(fips);
In this example, we’ve changed the default input type such that the app will read from ByteArrayInputStream and will not keep waiting for user input.
在本示例中,我们更改了默认输入类型,应用程序将从 ByteArrayInputStream 读取数据,而不会一直等待用户输入。
We can use any other type of InputStream in the test, for example, we can read from a file:
我们可以在测试中使用任何其他类型的InputStream,例如,我们可以从文件中读取:
InputStream fips = getClass().getClassLoader().getResourceAsStream("test-input.txt");
System.setIn(fips);
Moreover, with the same approach, we can replace the output stream in order to verify what the program writes:
此外,我们还可以用同样的方法替换输出流,以验证程序写入的内容:
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintStream out = new PrintStream(byteArrayOutputStream);
System.setOut(out);
With such an approach, we won’t see console output because System.out will send all the data to ByteArrayOutputStream instead of the console.
使用这种方法,我们将看不到控制台输出,因为 System.out 将把所有数据发送到字节数组输出流,而不是控制台。
5. Complete Test Example
5.完整测试示例
Let’s combine all the knowledge gathered in the previous paragraphs to write a complete test. These are the steps we are going to perform:
让我们把前面几段中收集到的所有知识结合起来,编写一个完整的测试。以下是我们要执行的步骤:
- Mock our main class as a spy
- Define input arguments as an array of String
- Replace default stream in System.in
- Verify that the program calls all required methods within the static context or that the program writes the necessary result to the console.
- Replace System.in and System.out streams back to original such that stream replacement will not affect other tests
In this example, we have a test for a StaticMain class where all logic is placed in a static context. We are replacing System.in with ByteArrayInputStream and building our verification based on the verify():
在本例中,我们对 StaticMain 类进行了测试,其中所有逻辑都置于静态上下文中。我们将 System.in 替换为 ByteArrayInputStream 并根据 verify() 建立验证:</em
@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);
}
}
We can use a slightly different strategy for the SimpleMain class because here we have distributed all business logic through other classes.
对于SimpleMain类,我们可以使用稍有不同的策略,因为在这里我们通过其他类分配了所有业务逻辑。
In this case, we do not even need to mock SimpleMain class because there are no other methods inside. We are replacing System.in with a file stream and building our verification based on the console output propagated to ByteArrayOutputStream:
在这种情况下,我们甚至不需要模拟 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. Conclusion
6.结论
In this article, we’ve explored several main method designs along with their corresponding testing approaches. We’ve covered testing for static and void methods, handling arguments, and changing default system streams.
在本文中,我们探讨了几种主要的方法设计及其相应的测试方法。我们介绍了静态方法和无效方法的测试、参数处理以及更改默认系统流。
The complete examples are available over on GitHub.
在 GitHub 上提供了完整的示例。