1. 概述

在单元测试中,我们有时需要验证通过 System.out.println() 输出到标准输出(standard output)的内容。

虽然在实际开发中,我们更推荐使用日志框架(如 Logback、SLF4J)而不是直接操作 System.out,但某些场景下(比如遗留系统、命令行工具、教学示例)我们仍不得不面对它。

本文将介绍几种 使用 JUnit 测试 System.out.println() 输出内容的有效方法。✅

2. 一个简单的打印方法

我们测试的目标是一个非常简单的打印方法:

private void print(String output) {
    System.out.println(output);
}

📌 补充一句:System.out 是一个 public static final PrintStream 类型的静态变量,代表 JVM 全局的标准输出流。它是共享的、可被重定向的——这一点正是我们能做测试的基础。

3. 使用原生 Java 实现测试

最直接的方式是利用 Java 标准库提供的 System.setOut() 动态替换输出流。核心思路是:

System.out 重定向到一个内存中的 ByteArrayOutputStream,执行目标方法后,从该流中读取输出内容进行断言。

✅ 实现步骤:

  1. 保存原始 System.out
  2. 设置新的 PrintStream 捕获输出
  3. 执行测试
  4. 断言输出内容
  5. 恢复原始 System.out(关键!⚠️)

示例代码:

private final PrintStream standardOut = System.out;
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();

@BeforeEach
public void setUp() {
    System.setOut(new PrintStream(outputStreamCaptor));
}

@Test
void givenSystemOutRedirection_whenInvokePrintln_thenOutputCaptorSuccess() {
    print("Hello Baeldung Readers!!");
        
    Assert.assertEquals("Hello Baeldung Readers!!", outputStreamCaptor.toString().trim());
}

@AfterEach
public void tearDown() {
    System.setOut(standardOut);
}

⚠️ 注意事项:

  • System.out.println() 会自动添加换行符(\n\r\n),所以要用 .trim() 去除前后空白,避免断言失败。
  • 必须在 @AfterEach 中恢复原始 System.out,否则会影响其他测试用例,造成“踩坑”式诡异问题。❌

✅ 优点:

  • 不依赖第三方库
  • 原理清晰,适合理解底层机制

❌ 缺点:

  • 模板代码多,重复性高
  • 容易忘记恢复流,导致测试污染

4. 使用 System Rules(JUnit 4)

如果你用的是 JUnit 4,推荐使用 System Rules 这个轻量级库。它通过 JUnit Rule 机制封装了流重定向的细节,使用起来更安全、简洁。

添加依赖:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-rules</artifactId>
    <version>1.19.0</version>
    <scope>test</scope>
</dependency>

使用 SystemOutRule 捕获输出:

@Rule
public final SystemOutRule systemOutRule = new SystemOutRule().enableLog();

@Test
public void givenSystemOutRule_whenInvokePrintln_thenLogSuccess() {
    print("Hello Baeldung Readers!!");

    Assert.assertEquals("Hello Baeldung Readers!!", systemOutRule.getLog().trim());
}

✅ 高级特性:

  • getLog():返回捕获的日志字符串(自动处理换行)
  • getLogWithNormalizedLineSeparator():统一换行为 \n,跨平台更稳定
Assert.assertEquals("Hello Baeldung Readers!!\n", systemOutRule.getLogWithNormalizedLineSeparator());

✅ 优点:

  • API 简洁,语义清晰
  • 自动管理资源,无需手动恢复 System.out
  • 支持 System.err、环境变量、系统属性等更多场景

❌ 缺点:

  • 仅支持 JUnit 4 的 @Rule 机制,不兼容 JUnit 5

5. 使用 System Lambda(JUnit 5 + Lambda)

JUnit 5 废弃了 @Rule,改用 Extension 模型。为此,原作者提供了 system-lambda —— 专为 JUnit 5 和函数式风格设计的升级版。

添加依赖:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-lambda</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

使用 tapSystemOut() 函数式捕获:

@Test
void givenTapSystemOut_whenInvokePrintln_thenOutputIsReturnedSuccessfully() throws Exception {
    String text = tapSystemOut(() -> {
        print("Hello Baeldung Readers!!");
    });

    Assert.assertEquals("Hello Baeldung Readers!!", text.trim());
}

✅ 核心优势:

  • 基于 lambda,代码块即作用域,自动完成流的重定向与恢复
  • 无需字段、无需 @BeforeEach/@AfterEach,真正零污染
  • 与 JUnit 5 完美集成,风格现代

📌 其他常用方法:

方法 用途
tapSystemOut() 捕获 System.out 输出
tapSystemErr() 捕获 System.err 输出
withTextFromSystemIn() 模拟标准输入
clearSystemProperties() 临时清除系统属性

适合在集成测试中模拟各种系统行为。

6. 总结

方式 适用场景 推荐指数
原生 Java 重定向 学习原理、无外部依赖 ⭐⭐⭐
System Rules(JUnit 4) JUnit 4 项目,追求简洁 ⭐⭐⭐⭐
System Lambda(JUnit 5) JUnit 5 + 函数式风格 ⭐⭐⭐⭐⭐ ✅

📌 最终建议

  • 如果你在使用 JUnit 5,直接上 system-lambda,简单粗暴又安全。
  • 如果是老项目用 JUnit 4system-rules 是成熟稳定的首选。
  • 原生方式适合理解原理,但不建议在生产测试中重复造轮子。

所有示例代码已托管至 GitHub:https://github.com/baeldung/tutorials/tree/master/testing-modules/testing-libraries


原始标题:Unit Testing of System.out.println() with JUnit