使用ByteArrayOutputStream的JUnit批量测试失败但单测通过的问题排查
Test set: org.oscargs.MenuTest
Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.452 s <<< FAILURE! -- in org.oscargs.MenuTest
org.oscargs.MenuTest.badCommandLenghtOneTest -- Time elapsed: 0.008 s <<< FAILURE!
org.opentest4j.AssertionFailedError:
expected: <Input command: Bad parameters! Only available parameters are 'start
but was: <>
at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1141)
at org.oscargs.MenuTest.badCommandLenghtOneTest(MenuTest.java:64)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
## 使用的依赖 ```xml <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency>
核心原因
- Log4j输出未被正确重置:被测类使用
Log4j的logger.info()输出内容,但测试仅替换了System.out,Log4j的全局配置在批量测试时会被复用,导致后续测试的日志未输出到当前测试创建的ByteArrayOutputStream。 - System.out恢复错误:
@AfterEach中执行System.setOut(null)会破坏标准输出流的状态,导致后续测试无法正常输出。
具体修复步骤
1. 保存并恢复原始System.out
在测试类中新增变量保存原始标准输出流,在@AfterEach中恢复,而非设置为null:
package org.oscargs; import org.junit.jupiter.api.*; import java.io.*; import static org.junit.jupiter.api.Assertions.*; class MenuTest { private Menu menu; private OutputStream outputStream; private PrintStream originalOut; // 新增:保存原始输出流 @BeforeEach void setUp() { try { originalOut = System.out; // 保存原始输出 this.outputStream = new ByteArrayOutputStream(); System.setOut(new PrintStream(outputStream)); this.menu = new Menu(); } catch (IOException e) { throw new RuntimeException(e); } } @AfterEach void tearDown() { try { System.setOut(originalOut); // 恢复原始输出流 this.outputStream.close(); this.menu.teardown(); this.menu = null; } catch (IOException e) { throw new RuntimeException(e); } } // 测试方法保持不变 @Test void badCommandLenghtOneTest() { // ... 原代码 } @Test void exitCommandTest() { // ... 原代码 } }
2. 动态配置Log4j,让日志输出到当前测试的OutputStream
因为被测类依赖Log4j,需要在每个测试前重新配置Log4j,使其输出到当前测试的ByteArrayOutputStream:
第一步:添加Log4j核心依赖(如果未添加)
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> <scope>test</scope> </dependency>
第二步:在@BeforeEach中配置Log4j
import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.appender.ConsoleAppender; import org.apache.logging.log4j.core.layout.PatternLayout; // ... 其他代码 @BeforeEach void setUp() { try { originalOut = System.out; this.outputStream = new ByteArrayOutputStream(); PrintStream testOut = new PrintStream(outputStream); System.setOut(testOut); // 配置Log4j输出到当前测试的PrintStream LoggerContext context = LoggerContext.getContext(false); Configuration config = context.getConfiguration(); // 创建仅输出消息内容的布局(避免多余日志格式干扰测试) PatternLayout layout = PatternLayout.newBuilder() .withPattern("%msg") // 只输出日志消息,不添加额外换行或格式 .build(); // 创建输出到System.out的Appender(此时System.out已指向测试的outputStream) ConsoleAppender appender = ConsoleAppender.newBuilder() .setName("TestConsoleAppender") .setTarget(ConsoleAppender.Target.SYSTEM_OUT) .setLayout(layout) .build(); appender.start(); config.addAppender(appender); // 将Appender绑定到根Logger config.getRootLogger().addAppender(appender, null, null); // 更新Log4j配置 context.updateLoggers(config); this.menu = new Menu(); } catch (IOException e) { throw new RuntimeException(e); } }
3. 调整预期输出匹配Log4j格式
因为Log4j的logger.info()不会自动添加换行(除非布局配置了),所以需要调整测试中的预期输出,确保和实际输出完全一致:
比如在badCommandLenghtOneTest中,预期输出应为:
String expectedOutput = "Input command: " + "Bad parameters! Only available parameters are 'start <player1> <player2>' and 'exit'\n";
额外优化
- 移除测试类中未使用的
@ExtendWith(MockitoExtension.class)注解,减少不必要的依赖。 - 去掉
teardown()中的outputStream.flush(),ByteArrayOutputStream在关闭时会自动刷新内容。
内容的提问来源于stack exchange,提问作者Oscar Garcia

