批量运行Java单测时Scanner.throwFor()抛NoSuchElementException问题
JUnit批量运行测试用例IO异常问题修复方案
问题根因
批量运行失败、单用例运行正常的核心原因有两点:
- 资源释放无保障:现有测试代码将
MockInOut.close()放在断言语句之后,一旦断言执行失败抛出异常,close方法不会被触发,System.in、System.out会停留在上一个用例设置的自定义流状态,后续用例拿到的要么是已经被置空的流引用,要么是上一个用例已经读取完毕的残留流,自然无法读取到模拟输入。 - Scanner状态污染:如果被测代码中使用了静态/单例级别的
Scanner实例绑定System.in,第一次用例运行时Scanner会将旧输入流的内容预读入自身缓冲区,即使后续用例替换了System.in指向新的模拟输入流,Scanner仍会持有旧流引用、读取已空的旧缓冲区,直接判定无数据可读,也就是异常栈中看到的needInput=false状态。
修复步骤
- 给MockInOut类实现
AutoCloseable接口,直接在类定义上加implements AutoCloseable即可,类中已有的close方法不需要修改。后续所有测试用例使用try-with-resources语法包裹MockInOut实例,保证无论用例执行成功、失败还是抛出异常,都能自动还原原始的System.in和System.out,不要手动在断言后调用close。修正后的用例结构参考:
@Test public void testInputMethod() { MyClassForConstructor myclassfc = new MyClassForConstructor(); MyClass myclass = new MyClass(myclassfc); // 自动管理MockInOut生命周期,退出try块时自动执行close还原流 try (MockInOut userkeyboardinput = new MockInOut("firstLineInput\nsecondLineInput\nthirdLineInput\n")) { MyObject myobToGetTested = myclass.methodThatRequiresInput(); MyObject myobExpected = new MyObject(par1, par2, par3); assertEquals(myobExpected.toString(), myobToGetTested.toString()); } }
- 检查被测代码的Scanner使用逻辑:禁止使用静态全局Scanner绑定System.in;如果必须持有全局Scanner实例,在测试类的
@BeforeEach方法中重置Scanner实例,强制其重新绑定当前最新的System.in流,避免持有旧流引用。 - 检查被测代码逻辑:不要主动关闭绑定System.in的Scanner实例,关闭Scanner会连带关闭底层的输入流,导致后续用例拿到已关闭的流句柄触发异常。绑定标准输入输出的流不需要业务代码手动关闭,JVM退出时会统一回收。
验证方式
修改完成后可在每个用例初始化MockInOut后打印System.in的对象哈希值,确认每个用例运行时拿到的都是新生成的ByteArrayInputStream实例,没有复用上一个用例的旧流即可。
内容的提问来源于stack exchange,提问作者devOn
相关产品推荐
相关产品推荐

