1. 概述
在编写单元测试时,我们有时会遇到需要测试那些直接与 System
类交互的代码。这种情况常见于命令行工具类应用中,比如直接调用 System.exit
或通过 System.in
读取输入。
本教程将介绍一个非常实用的外部库 —— System Rules,它提供了一组 JUnit 规则,用于测试那些使用 System
类的代码。我们将重点讲解其最常用的功能。
2. Maven 依赖配置
首先,我们需要在 pom.xml
中添加 System Rules 依赖:
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.19.0</version>
</dependency>
此外,我们还会引入 System Lambda 依赖,它支持 Maven Central:
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-lambda</artifactId>
<version>1.1.0</version>
</dependency>
✅ 由于 System Rules 并不直接支持 JUnit 5,所以我们引入 System Lambda 来实现兼容。
此外,还有一个基于 JUnit 5 Extensions 的替代方案 —— System Stubs。
3. 系统属性简介
简单回顾一下,Java 平台使用 Properties
对象来存储本地系统和配置信息。我们可以打印出这些属性:
System.getProperties()
.forEach((key, value) -> System.out.println(key + ": " + value));
输出示例:
java.version: 1.8.0_221
file.separator: /
user.home: /Users/baeldung
os.name: Mac OS X
...
我们也可以通过 System.setProperty
设置自己的系统属性。但需要注意的是,这些属性是 JVM 全局的。
⚠️ 如果我们在测试中修改了系统属性,务必在测试结束后将其恢复原值(无论测试是否成功),否则可能导致测试之间互相干扰。
接下来我们会看到如何通过 System Rules 简洁地设置、恢复系统属性。
4. 提供系统属性
假设我们有一个系统属性 log_dir
,用于指定日志写入路径,应用在启动时会设置它:
System.setProperty("log_dir", "/tmp/baeldung/logs");
4.1. 提供单个属性
在单元测试中,我们想提供一个不同的值,可以使用 ProvideSystemProperty
规则:
public class ProvidesSystemPropertyWithRuleUnitTest {
@Rule
public final ProvideSystemProperty providesSystemPropertyRule = new ProvideSystemProperty("log_dir", "test/resources");
@Test
public void givenProvideSystemProperty_whenGetLogDir_thenLogDirIsProvidedSuccessfully() {
assertEquals("log_dir should be provided", "test/resources", System.getProperty("log_dir"));
}
}
✅ 测试结束后,原属性值会自动恢复。比如我们在 @AfterClass
中打印:
@AfterClass
public static void tearDownAfterClass() throws Exception {
System.out.println(System.getProperty("log_dir"));
}
输出为:
/tmp/baeldung/logs
4.2. 提供多个属性
可以使用 and
方法链式提供多个属性:
@Rule
public final ProvideSystemProperty providesSystemPropertyRule =
new ProvideSystemProperty("log_dir", "test/resources").and("another_property", "another_value");
4.3. 从文件提供属性
也可以从文件或 classpath 资源加载属性:
@Rule
public final ProvideSystemProperty providesSystemPropertyFromFileRule =
ProvideSystemProperty.fromResource("/test.properties");
@Test
public void givenProvideSystemPropertyFromFile_whenGetName_thenNameIsProvidedSuccessfully() {
assertEquals("name should be provided", "baeldung", System.getProperty("name"));
assertEquals("version should be provided", "1.0", System.getProperty("version"));
}
假设 test.properties
内容如下:
name=baeldung
version=1.0
4.4. JUnit 5 与 Lambda 方式提供属性
在 JUnit 5 中,可以使用 System Lambda 提供的 restoreSystemProperties
方法:
@BeforeAll
static void setUpBeforeClass() throws Exception {
System.setProperty("log_dir", "/tmp/baeldung/logs");
}
@Test
void givenSetSystemProperty_whenGetLogDir_thenLogDirIsProvidedSuccessfully() throws Exception {
restoreSystemProperties(() -> {
System.setProperty("log_dir", "test/resources");
assertEquals("log_dir should be provided", "test/resources", System.getProperty("log_dir"));
});
assertEquals("log_dir should be provided", "/tmp/baeldung/logs", System.getProperty("log_dir"));
}
⚠️ 注意:这种方式不支持从文件加载属性。
5. 清除系统属性
有时候我们希望在测试开始时清除某些系统属性,并在测试结束后恢复它们:
@Rule
public final ClearSystemProperties userNameIsClearedRule = new ClearSystemProperties("user.name");
@Test
public void givenClearUsernameProperty_whenGetUserName_thenNull() {
assertNull(System.getProperty("user.name"));
}
✅ 可以同时清除多个属性,只需在构造器中传入多个属性名即可。
6. 模拟 System.in
在交互式命令行程序中,我们经常需要从 System.in
读取输入。System Rules 提供了 TextFromStandardInputStream
规则来模拟输入:
private String getFullname() {
try (Scanner scanner = new Scanner(System.in)) {
String firstName = scanner.next();
String surname = scanner.next();
return String.join(" ", firstName, surname);
}
}
模拟输入:
@Rule
public final TextFromStandardInputStream systemInMock = emptyStandardInputStream();
@Test
public void givenTwoNames_whenSystemInMock_thenNamesJoinedTogether() {
systemInMock.provideLines("Jonathan", "Cook");
assertEquals("Names should be concatenated", "Jonathan Cook", getFullname());
}
✅ 使用 provideLines
方法可传入多个输入值(varargs)。
JUnit 5 中使用 System Lambda:
@Test
void givenTwoNames_whenSystemInMock_thenNamesJoinedTogether() throws Exception {
withTextFromSystemIn("Jonathan", "Cook").execute(() -> {
assertEquals("Names should be concatenated", "Jonathan Cook", getFullname());
});
}
7. 测试 System.out
和 System.err
System Rules 提供了 SystemOutRule
和 SystemErrRule
来捕获标准输出和错误输出。
@Rule
public final SystemErrRule systemErrRule = new SystemErrRule().enableLog();
@Test
public void givenSystemErrRule_whenInvokePrintln_thenLogSuccess() {
printError("An Error occurred Baeldung Readers!!");
Assert.assertEquals("An Error occurred Baeldung Readers!!",
systemErrRule.getLog().trim());
}
private void printError(String output) {
System.err.println(output);
}
JUnit 5 使用 tapSystemErr
:
@Test
void givenTapSystemErr_whenInvokePrintln_thenOutputIsReturnedSuccessfully() throws Exception {
String text = tapSystemErr(() -> {
printError("An error occurred Baeldung Readers!!");
});
Assert.assertEquals("An error occurred Baeldung Readers!!", text.trim());
}
8. 处理 System.exit
命令行程序通常会调用 System.exit
来终止程序。如果测试中调用了它,会导致测试异常退出。
✅ 使用 ExpectedSystemExit
规则来优雅处理:
@Rule
public final ExpectedSystemExit exitRule = ExpectedSystemExit.none();
@Test
public void givenSystemExitRule_whenAppCallsSystemExit_thenExitRuleWorkssAsExpected() {
exitRule.expectSystemExitWithStatus(1);
exit();
}
private void exit() {
System.exit(1);
}
JUnit 5 中使用 catchSystemExit
:
@Test
void givenCatchSystemExit_whenAppCallsSystemExit_thenStatusIsReturnedSuccessfully() throws Exception {
int statusCode = catchSystemExit(() -> {
exit();
});
assertEquals("status code should be 1:", 1, statusCode);
}
9. 总结
本文详细介绍了 System Rules 库的核心功能:
- ✅ 提供和清除系统属性
- ✅ 模拟
System.in
输入 - ✅ 捕获
System.out
和System.err
输出 - ✅ 安全处理
System.exit
调用
此外,System Rules 还支持环境变量和安全管理器的模拟,建议查阅其 官方文档 获取更多信息。
源码地址:GitHub