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.outSystem.err

System Rules 提供了 SystemOutRuleSystemErrRule 来捕获标准输出和错误输出。

@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.outSystem.err 输出
  • ✅ 安全处理 System.exit 调用

此外,System Rules 还支持环境变量和安全管理器的模拟,建议查阅其 官方文档 获取更多信息。

源码地址:GitHub


原始标题:Guide to the System Rules Library