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
,执行目标方法后,从该流中读取输出内容进行断言。
✅ 实现步骤:
- 保存原始
System.out
- 设置新的
PrintStream
捕获输出 - 执行测试
- 断言输出内容
- 恢复原始
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 4,
system-rules
是成熟稳定的首选。 - 原生方式适合理解原理,但不建议在生产测试中重复造轮子。
所有示例代码已托管至 GitHub:https://github.com/baeldung/tutorials/tree/master/testing-modules/testing-libraries