
本文详细阐述了如何在java单元测试中处理依赖`system.in`进行用户交互的方法。通过重定向标准输入流`system.in`为`bytearrayinputstream`,我们可以模拟用户输入,并结合`bytearrayoutputstream`捕获标准输出,从而实现对交互式逻辑的自动化测试,有效解决测试阻塞问题,确保代码质量。
引言:测试System.in交互的挑战
在Java应用程序开发中,我们经常会遇到需要与用户进行交互的场景,例如通过命令行读取用户输入。Scanner类通常与System.in结合使用来获取这些输入。然而,当我们需要对包含System.in交互逻辑的方法进行单元测试时,会面临一个普遍的挑战:直接运行测试会导致程序在等待用户输入时无限期阻塞,使自动化测试无法完成。本文将深入探讨如何优雅地解决这一问题,通过重定向标准输入输出流来模拟用户交互,从而实现对这类方法的有效单元测试。
问题分析:为什么直接测试会阻塞?
考虑以下服务方法addBill,它使用Scanner从System.in读取用户选项:
@Overridepublic void addBill(Bill bill, Menu menu) { Scanner scanner = new Scanner(System.in); // ... System.out.print("Insert menu you want to be served: "); option = scanner.nextLine(); // 此处等待用户输入 // ...}
当你为addBill方法编写单元测试时,如果直接调用它,测试执行将会在scanner.nextLine()这一行停滞,因为System.in默认连接到键盘,而单元测试环境并没有提供键盘输入。这就会导致测试用例长时间处于“加载”状态,最终可能因超时而失败,或者需要手动干预才能继续。
核心策略:重定向标准输入输出流
解决System.in阻塞问题的核心策略是重定向Java的System.in和System.out流。我们可以将System.in替换为一个ByteArrayInputStream,该流可以预先加载我们希望模拟的用户输入数据。同时,为了验证方法的输出,我们可以将System.out替换为ByteArrayOutputStream,以便捕获方法打印到控制台的信息。
立即学习“Java免费学习笔记(深入)”;
模拟用户输入:ByteArrayInputStreamByteArrayInputStream允许我们从一个字节数组中读取数据,就像从文件中读取一样。通过将我们想要模拟的用户输入字符串转换为字节数组,并用它初始化ByteArrayInputStream,然后通过System.setIn()方法将这个流设置为新的标准输入流,即可实现模拟输入。
捕获系统输出:ByteArrayOutputStreamByteArrayOutputStream是一个输出流,它将数据写入一个内部的字节数组缓冲区。通过将System.out替换为一个PrintStream,而这个PrintStream又连接到ByteArrayOutputStream,我们就可以在测试结束后检查ByteArrayOutputStream中捕获到的内容,从而验证方法是否打印了预期的信息。
实战演练:逐步实现单元测试
我们将通过一个简化的账单服务示例来演示如何应用上述策略。
待测试服务类示例
为了简化,我们使用一个不依赖其他服务的BillingService,它直接处理用户输入和输出。
青柚面试
简单好用的日语面试辅助工具
57 查看详情
import java.io.InputStream;import java.util.Scanner;public class BillingService { public void addBill() { Scanner scanner = new Scanner(System.in); System.out.print("Insert menu you want to be served: "); var option = scanner.nextLine(); // 第一次输入 String menuOption = ""; // 初始化 menuOption if (!"e".equals(option)) { // 避免在退出时再次读取 System.out.print("Choose menu you want to eat/drink: "); menuOption = scanner.nextLine(); // 第二次输入 } try { switch (option) { case "1": System.out.println("Food Menu Selected."); switch (menuOption) { case "1" -> System.out.println("Breakfast selected."); case "2" -> System.out.println("Lunch selected."); case "3" -> System.out.println("Dinner selected."); default -> System.out.println("No food option found !!!"); } break; case "2": System.out.println("Drink Menu Selected."); switch (menuOption) { case "1" -> System.out.println("Alcohol selected."); case "2" -> System.out.println("Soft Drink selected."); default -> System.out.println("No drink option found !!!"); } break; case "e": System.out.println("Exiting bill service."); break; default: System.out.println("No main option found !!!"); break; } } catch (NullPointerException exception) { System.out.println("Error: " + exception.getMessage()); } finally { // 在实际应用中,不应关闭System.in关联的Scanner // 但在测试中,由于我们替换了System.in,这里通常可以忽略或由测试框架处理 } }}
测试环境搭建:@BeforeEach 和 @AfterEach
我们将使用JUnit 5和MockitoExtension。在每个测试方法执行前后,我们需要设置和恢复System.in和System.out。
import org.junit.jupiter.api.AfterEach;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.junit.jupiter.MockitoExtension;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.io.PrintStream;import static org.junit.jupiter.api.Assertions.assertTrue;@ExtendWith(MockitoExtension.class)class BillingServiceTest { private BillingService billingService; // 保存原始的System.in和System.out private final InputStream systemIn = System.in; private final PrintStream systemOut = System.out; // 用于模拟输入的流 private ByteArrayInputStream testIn; // 用于捕获输出的流 private ByteArrayOutputStream testOut; @BeforeEach public void init() { billingService = new BillingService(); // 在每个测试开始前,重定向System.out到testOut testOut = new ByteArrayOutputStream(); System.setOut(new PrintStream(testOut)); } @AfterEach public void restoreSystemInputOutput() { // 在每个测试结束后,恢复原始的System.in和System.out System.setIn(systemIn); System.setOut(systemOut); } // 辅助方法:提供模拟输入 private void provideInput(String data) { testIn = new ByteArrayInputStream(data.getBytes()); System.setIn(testIn); } @Test void whenChooseFoodMenuAndBreakfast_thenOutputCorrectMessage() { // 模拟用户输入 "1" (选择食物菜单) 和 "1" (选择早餐) provideInput("1n1"); billingService.addBill(); // 验证输出是否包含预期的消息 String actualOutput = testOut.toString(); assertTrue(actualOutput.contains("Food Menu Selected."), "Output should contain 'Food Menu Selected.'"); assertTrue(actualOutput.contains("Breakfast selected."), "Output should contain 'Breakfast selected.'"); } @Test void whenChooseDrinkMenuAndSoftDrink_thenOutputCorrectMessage() { // 模拟用户输入 "2" (选择饮料菜单) 和 "2" (选择软饮) provideInput("2n2"); billingService.addBill(); // 验证输出是否包含预期的消息 String actualOutput = testOut.toString(); assertTrue(actualOutput.contains("Drink Menu Selected."), "Output should contain 'Drink Menu Selected.'"); assertTrue(actualOutput.contains("Soft Drink selected."), "Output should contain 'Soft Drink selected.'"); } @Test void whenChooseExitOption_thenOutputExitMessage() { // 模拟用户输入 "e" (退出) provideInput("en"); // 即使只输入一个,也应提供换行符 billingService.addBill(); // 验证输出是否包含预期的消息 String actualOutput = testOut.toString(); assertTrue(actualOutput.contains("Exiting bill service."), "Output should contain 'Exiting bill service.'"); } @Test void whenChooseInvalidOption_thenOutputErrorMessage() { // 模拟用户输入 "x" (无效选项) 和任意后续输入 provideInput("xnany_inputn"); billingService.addBill(); String actualOutput = testOut.toString(); assertTrue(actualOutput.contains("No main option found !!!"), "Output should contain 'No main option found !!!'"); }}
注意事项与最佳实践
流的恢复至关重要: 在@AfterEach(或JUnit 4的@After)方法中恢复原始的System.in和System.out是极其重要的。如果忘记恢复,可能会影响后续的测试用例,甚至导致测试运行器或其他依赖System.in/System.out的组件行为异常。
提供完整的输入: 模拟输入字符串时,确保包含所有预期的换行符(n),以模拟用户在每次输入后按下回车键的行为。例如,如果方法需要两次输入,那么provideInput的参数应该像”option1noption2n”这样。
对于复杂依赖的模拟: 示例中的BillingService是独立的。在原始问题中,BillServiceImpl依赖于billItemServices和menuPrinter。对于这些外部依赖,应继续使用Mockito等模拟框架进行模拟,例如:
@Mockprivate BillItemServices billItemServices;@Mockprivate MenuPrinter menuPrinter;// ... 在测试中注入这些Mock对象
这样可以将对System.in的测试与对其他业务逻辑的测试解耦。
设计可测试的代码: 虽然上述方法解决了测试System.in的问题,但从长远来看,最佳实践是设计更易于测试的代码。这意味着将I/O操作与核心业务逻辑分离。例如,可以引入一个InputProvider接口:
public interface InputProvider { String readLine();}public class ConsoleInputProvider implements InputProvider { private final Scanner scanner = new Scanner(System.in); @Override public String readLine() { return scanner.nextLine(); }}public class BillServiceImpl { private final InputProvider inputProvider; public BillServiceImpl(InputProvider inputProvider) { this.inputProvider = inputProvider; } // ... option = inputProvider.readLine(); // 使用依赖注入的InputProvider // ...}
在测试时,可以提供一个MockInputProvider,它返回预设的字符串,而无需直接修改System.in。这种设计模式(依赖注入)大大提高了代码的可测试性和灵活性。
总结
对包含System.in交互逻辑的Java方法进行单元测试,需要巧妙地利用Java的I/O重定向机制。通过将System.in替换为ByteArrayInputStream来模拟用户输入,并将System.out替换为ByteArrayOutputStream来捕获输出,我们可以有效地对这类交互式逻辑进行自动化测试。结合@BeforeEach和@AfterEach(或JUnit 4的@Before和@After)来管理测试环境的设置和恢复,可以确保测试的隔离性和稳定性。同时,我们也应该认识到,更好的设计模式(如依赖注入)可以从根本上提高代码的可测试性,减少对这种I/O重定向技巧的依赖。
以上就是Java单元测试实战:应对System.in交互式输入的挑战的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/298743.html
微信扫一扫
支付宝扫一扫